From 83c569b231d3ef4b198116426ff0e698c555e92b Mon Sep 17 00:00:00 2001 From: choibk Date: Sat, 6 Dec 2025 18:58:09 +0900 Subject: [PATCH] =?UTF-8?q?13.=20=EC=83=81=ED=92=88=EC=88=98=EC=A0=95?= =?UTF-8?q?=EA=B9=8C=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/LoginController.php | 57 +++++++ app/Http/Controllers/ProductController.php | 139 ++++++++++++++++++ app/Http/Middleware/Authenticate.php | 21 +++ app/Models/Product.php | 16 ++ app/Providers/AppServiceProvider.php | 3 +- bootstrap/app.php | 2 +- config/app.php | 2 +- ...025_12_06_032222_create_products_table.php | 39 +++++ database/seeders/DatabaseSeeder.php | 6 +- resources/views/dashboard.blade.php | 66 +++++++++ resources/views/inc/topbar.blade.php | 37 +++++ resources/views/layout.blade.php | 18 +++ resources/views/login.blade.php | 15 +- resources/views/product/edit.blade.php | 49 ++++++ resources/views/product/input.blade.php | 54 +++++++ resources/views/product/list.blade.php | 95 ++++++++++++ routes/web.php | 46 +++++- 17 files changed, 648 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/LoginController.php create mode 100644 app/Http/Controllers/ProductController.php create mode 100644 app/Http/Middleware/Authenticate.php create mode 100644 app/Models/Product.php create mode 100644 database/migrations/2025_12_06_032222_create_products_table.php create mode 100644 resources/views/dashboard.blade.php create mode 100644 resources/views/inc/topbar.blade.php create mode 100644 resources/views/layout.blade.php create mode 100644 resources/views/product/edit.blade.php create mode 100644 resources/views/product/input.blade.php create mode 100644 resources/views/product/list.blade.php diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php new file mode 100644 index 0000000..ca0f6fe --- /dev/null +++ b/app/Http/Controllers/LoginController.php @@ -0,0 +1,57 @@ +route('dashboard'); + } + + return view('login'); + } + + public function login(Request $request) + { + try { + $credentials = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]); + } catch (ValidationException $e) { + return redirect() + ->back() + ->withErrors($e->errors()) + ->withInput() + ->with('error', '입력 값 검증 실패'); + } + + if (Auth::attempt($credentials, false)) { + $request->session()->regenerate(); + + return redirect() + ->intended('/') + ->with('success', '로그인에 성공하였습니다.'); + } + + return redirect() + ->back() + ->withInput() + ->with('error', '입력한 자격증명이 올바르지 않습니다.'); + } + public function logout(Request $request) + { + Auth::logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login'); + } +} diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php new file mode 100644 index 0000000..4f693f6 --- /dev/null +++ b/app/Http/Controllers/ProductController.php @@ -0,0 +1,139 @@ +validate([ + 'name' => ['required', 'string'], + 'sku' => [ + 'required', + 'string', + Rule::unique('products', 'sku')->ignore($product->id), // 현재 상품은 예외 + ], + 'price' => ['required', 'numeric', 'min:0'], + 'image' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], + ]); + + // 2) 텍스트 데이터 업데이트 + $product->name = $data['name']; + $product->sku = $data['sku']; + $product->price = $data['price']; + + // 3) 이미지 처리 + if ($request->hasFile('image')) { + + // 기존 이미지가 있으면 삭제 + if ($product->image && Storage::disk('public')->exists($product->image)) { + Storage::disk('public')->delete($product->image); + // Storage 파사드를 이용하면, 저장소 드라이버가 바뀌어도 코드 수정 최소화 + } + + // 새 이미지 저장 + $path = $request->file('image')->store('uploads', 'public'); + $product->image = $path; + } + + // 4) 저장 + $product->save(); + + // 5) 결과 응답 + return redirect() + ->route('product') + ->with('success', '상품정보가 수정되었습니다.'); + } + + public function destroy($id) + { + // 1. 모델을 먼저 가져온다 + $product = Product::findOrFail($id); + + // 2. 이미지가 있는 경우만 삭제 + if (!empty($product->image)) { + Storage::disk('public')->delete($product->image); + } + + // 3. DB 레코드 삭제 + $product->delete(); + + // 4. 목록으로 이동 + return redirect() + ->route('product') + ->with('success', '상품이 삭제되었습니다.'); + } + + public function store(Request $request) + { + // 1) 검증 + 중복 검사 + 사용자 정의 메시지 + $data = $request->validate([ + 'name' => ['required', 'string', 'max:100'], + 'sku' => ['required', 'string', 'max:255', 'unique:products,sku'], + 'quantity' => ['required', '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' => '수량은 필수 입력 항목입니다.', + 'quantity.numeric' => '수량은 숫자여야 합니다.', + 'quantity.min' => '수량은 0 이상이어야 합니다.', + + // price + 'price.required' => '가격은 필수 입력 항목입니다.', + 'price.numeric' => '가격은 숫자여야 합니다.', + 'price.min' => '가격은 0 이상이어야 합니다.', + + // image + 'image.image' => '업로드된 파일은 이미지여야 합니다.', + 'image.max' => '이미지 파일 크기는 2MB 이하만 가능합니다.', + ]); + + // 2) 이미지 업로드 처리 + if ($request->hasFile('image')) { + $data['image'] = $request->file('image')->store('products', 'public'); + // 저장 경로 예: storage/app/public/products/파일명.jpg + } + + // 3) DB 저장 + Product::create($data); + + // 4) 등록 후 리다이렉트 + return redirect() + ->route('product.input') + ->with('success', '상품이 등록되었습니다.'); + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..1935070 --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,21 @@ +expectsJson()) { + return route('login'); // routes/web.php 에서 name('login') 사용 중 + } + + return null; + } +} + diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 0000000..14042a9 --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,16 @@ +paginate(1); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..9aa3fe9 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Pagination\Paginator; class AppServiceProvider extends ServiceProvider { @@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Paginator::useBootstrap(); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..c5c7a1e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,7 +11,7 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + // 커스텀 alias 전부 제거 – 비워둠 }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/config/app.php b/config/app.php index 423eed5..3adeaa5 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => 'Asia/Seoul', /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2025_12_06_032222_create_products_table.php b/database/migrations/2025_12_06_032222_create_products_table.php new file mode 100644 index 0000000..9083db6 --- /dev/null +++ b/database/migrations/2025_12_06_032222_create_products_table.php @@ -0,0 +1,39 @@ +id(); // 기본 PK, auto_increment + + $table->string('name', 100)->comment('상품명'); + + $table->string('sku') + ->unique() + ->comment('SKU'); + + $table->integer('quantity') + ->default(0) + ->comment('수량'); + + $table->integer('price') + ->comment('가격'); + + $table->string('image') + ->nullable() + ->comment('상품이미지'); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..7cf3156 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,6 +5,7 @@ namespace Database\Seeders; use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { @@ -18,8 +19,9 @@ class DatabaseSeeder extends Seeder // User::factory(10)->create(); User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + 'name' => 'sokuree', + 'email' => 'sokuree@sokuree.com', + 'password' => Hash::make('password') ]); } } diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php new file mode 100644 index 0000000..6f08ce2 --- /dev/null +++ b/resources/views/dashboard.blade.php @@ -0,0 +1,66 @@ + @extends('layout') + + @section('main') + {{-- 페이지 제목 --}} +
+

📦 재고관리 대시보드

+ + {{-- 요약 카드 --}} +
+
+
+
+
총 상품 수
+

00

+
+
+
+ +
+
+
+
전체 재고 수량
+

000

+
+
+
+
+ + {{-- 최근 입출고 이력 --}} +
+
+
🕓 최근 입출고 이력
+ 전체 보기 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
상품명유형수량날짜
상품명1입고00YYYY-mm-dd HH:ii:ss
상품명2출고00YYYY-mm-dd HH:ii:ss
+
+
+ +
+@endsection \ No newline at end of file diff --git a/resources/views/inc/topbar.blade.php b/resources/views/inc/topbar.blade.php new file mode 100644 index 0000000..d43ff82 --- /dev/null +++ b/resources/views/inc/topbar.blade.php @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php new file mode 100644 index 0000000..7a549bf --- /dev/null +++ b/resources/views/layout.blade.php @@ -0,0 +1,18 @@ + + + + + {{ $title ?? '대시보드' }} + + + + +@include('inc.topbar') + +
+ +@yield('main') + +
+ + diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php index d0c3a36..6c63cd4 100644 --- a/resources/views/login.blade.php +++ b/resources/views/login.blade.php @@ -10,17 +10,18 @@

관리자 로그인

- -
메시지
- -
+ @if (session('error')) +
{{session('error')}}
+ @endif + + @csrf()
- - + +
- +
diff --git a/resources/views/product/edit.blade.php b/resources/views/product/edit.blade.php new file mode 100644 index 0000000..930ad50 --- /dev/null +++ b/resources/views/product/edit.blade.php @@ -0,0 +1,49 @@ +@extends('layout') + +@section('main') +
+

📝 상품 수정

+ +
+ @csrf + @method('PUT') +
+ + + @error('name') +
{{ $message }}
+ @enderror +
+
+ + + @error('sku') +
{{ $message }}
+ @enderror +
+
+ + + @error('price') +
{{ $message }}
+ @enderror +
+
+ + + {{-- 기존 이미지 미리보기 --}} + @if($product->image) +
+ +
+ @endif +
+ +
+ + 취소 +
+
+
+@endsection diff --git a/resources/views/product/input.blade.php b/resources/views/product/input.blade.php new file mode 100644 index 0000000..9f68145 --- /dev/null +++ b/resources/views/product/input.blade.php @@ -0,0 +1,54 @@ +@extends('layout') + +@section('main') + +
+

📝 상품 추가

+ +
+ @csrf + +
+ + + @error('name') +
{{ $message }}
+ @enderror +
+
+ + + @error('sku') +
{{ $message }}
+ @enderror +
+
+ + + @error('quantity') +
{{ $message }}
+ @enderror +
+
+ + + @error('price') +
{{ $message }}
+ @enderror +
+
+ + +
+ 상품 이미지 +
+
+ +
+ + 취소 +
+
+
+ +@endsection diff --git a/resources/views/product/list.blade.php b/resources/views/product/list.blade.php new file mode 100644 index 0000000..940b963 --- /dev/null +++ b/resources/views/product/list.blade.php @@ -0,0 +1,95 @@ +@extends('layout') + +@section('main') +
+
+

📋 상품 목록

+ + 상품 추가 +
+ + @if(session('success')) +
{{ session('success') }}
+ @endif + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + @foreach ($products as $product) + + + + + + + + + @endforeach + +
ID상품명SKU수량가격관리
{{ $product->id }}{{ $product->name }}{{ $product->sku }}{{ number_format($product->quantity) }} 개{{ number_format($product->price) }} 원 + +
+
+ + {{ $products->links() }} + +
+ + {{-- 공용 삭제 폼 --}} + + +@endsection diff --git a/routes/web.php b/routes/web.php index 2a3ef07..43449be 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,11 +1,47 @@ middleware(Authenticate::class) + ->name('dashboard'); + +// ② 로그인 화면 (누구나 접근 가능) +Route::get('/login', [LoginController::class, 'showLoginForm']) + ->name('login'); + +// ③ 로그인 처리 (POST) +Route::post('/login', [LoginController::class, 'login']) + ->name('login.process'); + +// 로그아웃 +Route::post('/logout', [LoginController::class, 'logout']) + ->name('logout'); + +// 상품추가 +Route::get('/product/input', [ProductController::class, 'input']) + ->name('product.input'); + +Route::post('/product/input', [ProductController::class, 'store']) + ->name('product.store'); + +// 상품목록 +Route::get('/product', [ProductController::class, 'index']) + ->name('product'); + +// 상품삭제 +Route::delete('/product/{id}', [ProductController::class, 'destroy']) + ->name('product.delete'); + +// 상품수정 Route Model Binding +Route::get('/product/{product}/edit', [ProductController::class, 'edit']) + ->name('product.edit'); +Route::put('/product/{product}', [ProductController::class, 'update']) + ->name('product.update'); + -Route::get('/login', function () { - return view('login'); -});