commit for day 1

This commit is contained in:
2026-04-12 18:58:03 +09:00
commit 8bbdeafe9c
37 changed files with 3190 additions and 0 deletions

203
module-b/module-b-en.md Normal file
View File

@@ -0,0 +1,203 @@
# 🛍️ Test Project: Module B — ShopPress (Online Shopping Mall)
## 1. Project Overview
In the rapidly growing e-commerce market, the startup **ShopPress** is launching its own shopping mall platform that allows administrators to manage products and orders directly. Administrators can register and manage products and process orders, while general visitors can browse and purchase products. In this project, competitors are responsible for everything from database design to core feature implementation. This is an MVP, so focus on implementing the essential functionality. **Design is not a graded criterion.**
---
## 2. Tech Stack & Constraints
* **Tech Stack**: There are no restrictions on programming language or framework. You may freely choose any server-side rendering (SSR)-capable framework such as Laravel, Django, Ruby on Rails, or Spring Boot.
* **Desktop Environment**: Responsive design is not required. The application only needs to work in a desktop Chrome browser.
* **Image Upload**: Product images must be JPG or PNG format only, with a file size limit of **5MB**. This validation must be performed **server-side**.
* **Database**: Categories must be managed in a separate table (`categories`). The products table (`products`) must reference `category_id` as a foreign key (FK).
* **Initial Data**: The following data must be created automatically on first run (via Seeder or Migration).
* Admin account: Name `Admin`, Email `admin@shoppress.local`, Password `password` (password must be stored as a hash)
* At least 3 initial categories (e.g., `Clothing`, `Electronics`, `Food`)
---
## 3. Tasks to Complete
### **1. Authentication**
#### 1.1 Login and Session Management
The login page is accessible at `/admin/login`. The user enters their email and password. On successful login, the user is redirected to the admin dashboard. On failure, display the message **"Incorrect email or password."**
Accessing any route under `/admin` without being logged in redirects the user to the login page. Logging out destroys the session and redirects to the login page.
The currently logged-in admin's name must be displayed in the navigation on all admin pages.
---
### **2. Admin Features**
#### 2.1 Dashboard
The dashboard is the default admin page (`/admin` or `/admin/dashboard`) and is the first page shown after login.
Display the following 4 statistics as cards on the dashboard.
| Card Label | Content |
| :--- | :--- |
| **Total Products** | Total number of products |
| **Active Products** | Number of products with status `Active` |
| **Total Orders** | Total number of orders |
| **Total Revenue** | Sum of all final payment amounts (after coupon discounts) |
Also display a list of the 5 most recently placed orders. Each row must include: Order # , Customer, Total, Status, and Date.
#### 2.2 Product Management
The product management page (`/admin/products`) displays all products in a table.
Table columns: **Name**, **Category**, **Price**, **Stock**, **Status**, **Created At**, Edit / Delete buttons.
Provide a search field to filter products by name. Submitting a search term displays only products whose name contains that keyword.
The **product registration form** (`/admin/products/new`) and **product edit form** (`/admin/products/{id}/edit`) must include the following fields.
* **Name** (required, max 100 characters)
* **Category** (required, dropdown — populated from categories in the database)
* **Price** (required, integer, 0 or greater)
* **Stock** (required, integer, 0 or greater)
* **Description** (`<textarea>`, optional)
* **Image** (file upload, JPG/PNG, max 5MB, **optional** — products can be registered without an image)
* **Status** (dropdown: `Active` / `Out of Stock` / `Hidden`)
When entering the edit form, existing data must be pre-filled in each field. If no new image is uploaded, the existing image is retained. If a new image is uploaded, delete the existing image file from the server and replace it with the new one.
When deleting a product, display the confirmation message **"Are you sure you want to delete this product?"** On confirmation, if the product appears in any order (`order_items`), reject the deletion and display **"Cannot delete a product with order history."** If the product has no order history, delete it from the database and remove its image file from the server.
#### 2.3 Category Management
The category management page (`/admin/categories`) allows adding, editing, and deleting categories.
* **Add**: Enter a category name and save. If the name already exists, display **"Category name already exists."**
* **Edit**: Change the category name. Since products reference the category via FK, updating the category name in the `categories` table is automatically reflected in associated products.
* **Delete**: If any product references this category, reject the deletion and display **"Cannot delete a category that has products assigned."**
#### 2.4 Coupon Management
The coupon management page (`/admin/coupons`) allows registering and managing discount coupons.
The **coupon registration form** includes the following fields.
* **Coupon Code** (required, uppercase letters or digits only, 612 characters, must be unique)
* **Discount Type** (dropdown: `Fixed` / `Percent`)
* **Discount Value** (required, integer, 1 or greater. If `Percent`, must be between 1 and 100)
* **Minimum Order Amount** (required, integer, 0 or greater. Coupon cannot be applied to orders below this amount)
* **Expires At** (required, `<input type="date">`. Only future dates may be selected)
* **Usage Limit** (required, integer, 1 or greater)
Coupon list table columns: **Code**, **Type**, **Value**, **Min. Order**, **Expires At**, **Remaining Uses**, Delete button. Remaining uses are managed via the `remaining_uses` column, initialized to the Usage Limit value on registration and decremented by 1 each time the coupon is used.
#### 2.5 Order Management
The order management page (`/admin/orders`) displays all orders in a table.
Table columns: **Order #**, **Customer**, **Phone**, **Total**, **Status**, **Date**.
Provide a dropdown to filter orders by status. Status values are `Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`. Only orders matching the selected status are shown.
The order detail page (`/admin/orders/{id}`) must display the following information.
* Customer Info: Name, Phone, Shipping Address
* Order Items: Product (product name snapshot), Unit Price, Qty, Subtotal
* Coupon: Applied coupon code and discount amount. Display **"None"** if no coupon was applied.
* Total: Final payment amount
* **Order Status** dropdown (`Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`) and a Save button. On successful save, display **"Order status updated."** on the same page.
* When the status is changed to **`Cancelled`**, restore the stock of all products in the order by their ordered quantities. If the order is already `Cancelled` and the status is set to `Cancelled` again, do not restore stock a second time.
---
### **3. Public Pages**
#### 3.1 Home — Product List
The home page (`/`) displays products with status **`Active`** as cards. Each card shows the product image (display a grey placeholder area if no image exists), product name, category name, and price.
Display **12 items per page** with pagination. Sort by most recently registered first.
Provide category filter links. Clicking a category shows only **`Active`** products in that category. Pagination must retain the current category filter.
Provide a keyword search field. Products are searched by name. The category filter and keyword search must be applied simultaneously. Pagination must retain the search conditions.
#### 3.2 Product Detail
The product detail page is accessible at `/products/{id}`. Return a 404 page for non-existent IDs or products with status **`Hidden`**.
The product detail page displays the product image (grey placeholder if no image), product name, category name, price, stock quantity, and description.
Products with stock of 0 or status **`Out of Stock`** show an **"Out of Stock"** message and have the purchase button disabled.
Products with stock of 1 or more and status **`Active`** show a quantity input (`<input type="number">`, min 1, max current stock) and a **"Buy Now"** button. Clicking the button passes the product ID and quantity as query strings and navigates to the checkout page (`/checkout?product_id={id}&quantity={n}`).
#### 3.3 Checkout
The checkout page (`/checkout`) reads `product_id` and `quantity` from the query string and displays the order summary (product name, unit price, quantity, subtotal). The server must validate the following conditions and redirect to the home page (`/`) if any are not met.
* The product with `product_id` does not exist
* The product status is **`Hidden`** or **`Out of Stock`**
* Stock is 0
* `quantity` is less than 1 or exceeds current stock
The page includes a form for the buyer's Name, Phone, and Shipping Address. All fields are required and orders are only processed after passing **server-side validation**.
There is a **Coupon Code** input field and an **"Apply"** button. When "Apply" is clicked, use `fetch` to send a POST request to the `/checkout/coupon` endpoint in the format below, and reflect the result on the page without a full reload. This endpoint must be implemented by the competitor.
* Request Body (JSON): `{ "coupon_code": "ABCD1234", "subtotal": 30000 }`
* Success Response (JSON): `{ "valid": true, "discount_type": "fixed"|"percent", "discount_value": 3000, "discount_amount": 3000, "final_amount": 27000 }`
* Failure Response (JSON): `{ "valid": false, "message": "error message" }`
* Valid coupon: Calculate the discount and display the updated Subtotal, Discount, and Total on the page.
* `Fixed` discount: Subtract the discount value directly from the subtotal.
* `Percent` discount: Deduct `subtotal × discount rate / 100`. Round down (floor) any decimal.
* The final amount must not go below 0.
* Invalid coupon: Display the appropriate error message for each case.
* Code does not exist: **"Invalid coupon code."**
* Coupon has expired: **"This coupon has expired."**
* Usage limit reached: **"This coupon has reached its usage limit."**
* Below minimum order amount: **"This coupon requires a minimum order of {n}."**
Process the following as a **single transaction** when submitting the order.
1. Save order information to the `orders` table (buyer name, phone, shipping address, applied coupon ID — NULL if none, discount amount — 0 if none, final payment amount).
2. Save order item information to the `order_items` table (order ID, product ID, **product name snapshot**, quantity, unit price). The product name must be saved as it was at the time of the order so that it displays correctly even if the product is later deleted or renamed.
3. Deduct the ordered quantity from the product's stock (`stock`).
4. If a coupon was applied, decrement the coupon's `remaining_uses` by 1.
If stock is insufficient for the requested quantity, roll back the transaction and display **"Not enough stock."**
On successful order submission, redirect to the order complete page (`/orders/{id}/complete`). This page is accessible without authentication and displays the Order #, order item details, Discount, and Total.
#### 3.4 Layout
All public pages include a header and footer. The header contains the site name (**ShopPress**) and a link to the home page. Each page must have an appropriate `<title>` tag reflecting the page content.
Admin panel pages include a sidebar or top navigation. Navigation menu items: **Dashboard**, **Products**, **Categories**, **Coupons**, **Orders**.
---
## 4. Submission Guidelines
Upload all deliverables to the server directory `/module_b/`.
---
## 5. Marking Scheme — Total 25 Points
| Item | Criteria | Points |
| :--- | :--- | :---: |
| **A. Authentication** | Login/logout, session protection, unauthenticated access redirect | 2 |
| **B. Dashboard** | 4 statistics cards (English labels), recent 5 orders list | 1 |
| **C. Product Management** | List (with search), register (optional image), edit (retain/replace+delete old image), delete (reject if order history exists + delete image file) | 5 |
| **D. Category Management** | Add (duplicate check), edit, delete (reject if products assigned) | 2 |
| **E. Coupon Management** | Register (full validation), list (remaining uses display), delete | 3 |
| **F. Order Management** | List (status filter), detail view, status update, stock restore on Cancelled (prevent duplicate restore) | 4 |
| **G. Public — Product List** | Active product filtering, category filter, keyword search, pagination (conditions retained) | 3 |
| **H. Public — Product Detail** | Product info display, Out of Stock handling (stock 0 or Out of Stock status), quantity input and checkout navigation | 1 |
| **I. Public — Checkout** | Entry validation (Out of Stock/Hidden/stock exceeded), coupon application (fetch + discount calculation), transaction (save order + deduct stock + deduct coupon uses), order complete page | 3 |
| **J. Validation** | Server-side image size/extension check, checkout form field validation | 1 |
| **Total** | | **25** |

203
module-b/module-b-kr.md Normal file
View File

@@ -0,0 +1,203 @@
# 🛍️ Test Project: Module B — ShopPress (온라인 쇼핑몰)
## 1. 프로젝트 개요 (Project Overview)
빠르게 성장하는 이커머스 시장에서 스타트업 **ShopPress**는 관리자가 상품과 주문을 직접 운영할 수 있는 자체 쇼핑몰 플랫폼을 런칭하려 합니다. 관리자는 상품을 등록/관리하고 주문을 처리하며, 일반 방문자는 상품을 탐색하고 구매할 수 있습니다. 본 과제에서 선수는 데이터베이스 설계부터 핵심 기능 구현까지 전담합니다. 이 프로젝트는 MVP이므로 핵심 기능 구현에 집중하십시오. **디자인은 채점 항목이 아닙니다.**
---
## 2. 기술 스택 및 제약 조건 (Tech Stack & Constraints)
* **기술 스택**: 사용 언어 및 프레임워크에 제한이 없습니다. Laravel, Django, Ruby on Rails, Spring Boot 등 서버 사이드 렌더링(SSR)이 가능한 프레임워크를 자유롭게 선택할 수 있습니다.
* **데스크톱 환경**: 반응형 디자인은 구현하지 않으며, 데스크톱 Chrome 브라우저에서만 동작하면 됩니다.
* **이미지 업로드**: 상품 이미지는 JPG, PNG 형식만 허용하며, 파일 크기는 **5MB** 이하로 제한됩니다. 이 검사는 **서버 측**에서 수행해야 합니다.
* **데이터베이스**: 카테고리는 별도 테이블(`categories`)로 관리하고, 상품 테이블(`products`)은 `category_id`를 외래 키(FK)로 참조하는 구조로 설계하십시오.
* **초기 데이터**: 애플리케이션 최초 실행 시(Seeder 또는 Migration) 다음 데이터가 자동으로 생성되어야 합니다.
* 관리자 계정: 이름 `Admin`, 이메일 `admin@shoppress.local`, 비밀번호 `password` (비밀번호는 반드시 해시하여 저장)
* 초기 카테고리 3개 이상 (예: `Clothing`, `Electronics`, `Food`)
---
## 3. 과제 요구사항 (Tasks to Complete)
### **1. 인증 (Authentication)**
#### 1.1 로그인 및 세션 관리
로그인 페이지는 `/admin/login`에서 접근 가능합니다. 사용자는 이메일과 비밀번호를 입력합니다. 로그인 성공 시 관리자 대시보드로 이동합니다. 로그인 실패 시 **"Incorrect email or password."** 메시지를 표시합니다.
`/admin` 하위 경로에 로그인 없이 접근하면 로그인 페이지로 리다이렉트됩니다. 로그아웃 시 세션이 파기되고 로그인 페이지로 이동합니다.
현재 로그인한 관리자 이름은 모든 관리자 페이지의 내비게이션에 표시되어야 합니다.
---
### **2. 관리자 기능 (Admin Features)**
#### 2.1 대시보드
대시보드는 관리자 패널의 기본 페이지(`/admin` 또는 `/admin/dashboard`)이며, 로그인 후 첫 번째로 표시되는 페이지입니다.
대시보드에는 다음 4가지 통계를 카드 형태로 표시하십시오.
| 카드 레이블 | 내용 |
| :--- | :--- |
| **Total Products** | 전체 상품 수 |
| **Active Products** | 상태가 `Active`인 상품 수 |
| **Total Orders** | 전체 주문 수 |
| **Total Revenue** | 전체 누적 주문 금액(쿠폰 할인 적용 후 최종 결제 금액 합산 기준) |
또한 가장 최근 접수된 주문 5건의 목록을 표시하십시오. 각 항목에는 Order #(주문번호), Customer(구매자명), Total(최종 금액), Status(주문상태), Date(주문일시)가 포함되어야 합니다.
#### 2.2 상품 관리
상품 관리 페이지(`/admin/products`)는 전체 상품 목록을 테이블로 표시합니다.
테이블 컬럼: **Name**, **Category**, **Price**, **Stock**, **Status**, **Created At**, Edit / Delete buttons.
상품명을 기준으로 검색할 수 있는 검색창을 제공하십시오. 검색어를 입력하고 제출하면 해당 키워드가 포함된 상품만 목록에 표시됩니다.
**상품 등록 폼** (`/admin/products/new`)과 **상품 수정 폼** (`/admin/products/{id}/edit`)은 다음 필드를 포함해야 합니다.
* **Name** (필수, 최대 100자)
* **Category** (필수, 드롭다운 선택 — DB에 등록된 카테고리 목록으로 구성)
* **Price** (필수, 정수, 0 이상)
* **Stock** (필수, 정수, 0 이상)
* **Description** (`<textarea>`, 선택 항목)
* **Image** (파일 업로드, JPG/PNG, 5MB 이하, **선택 항목** — 이미지 없이도 상품 등록 가능)
* **Status** (드롭다운: `Active` / `Out of Stock` / `Hidden`)
수정 폼 진입 시 기존 데이터가 각 필드에 미리 채워져야 합니다. 이미지 파일을 새로 업로드하지 않으면 기존 이미지를 유지합니다. 새 이미지를 업로드하면 기존 이미지 파일을 서버에서 삭제하고 새 이미지로 교체합니다.
상품 삭제 시 **"Are you sure you want to delete this product?"** 확인 메시지를 표시합니다. 확인 시 해당 상품이 포함된 주문(`order_items`)이 1건 이상 존재하면 삭제를 거부하고 **"Cannot delete a product with order history."** 오류 메시지를 표시합니다. 주문 이력이 없는 상품은 DB에서 삭제하고 대표 이미지 파일도 서버에서 함께 삭제합니다.
#### 2.3 카테고리 관리
카테고리 관리 페이지(`/admin/categories`)에서 카테고리를 추가/수정/삭제할 수 있습니다.
* **추가**: 카테고리 이름을 입력하고 저장합니다. 이름이 중복될 경우 **"Category name already exists."** 오류 메시지를 표시합니다.
* **수정**: 카테고리 이름을 변경할 수 있습니다. `categories` 테이블의 해당 행 이름만 변경하면 FK로 연결된 상품에 자동으로 반영됩니다.
* **삭제**: 해당 카테고리를 참조하는 상품이 1개 이상 존재할 경우 삭제를 거부하고 **"Cannot delete a category that has products assigned."** 오류 메시지를 표시합니다.
#### 2.4 쿠폰 관리
쿠폰 관리 페이지(`/admin/coupons`)에서 할인 쿠폰을 등록하고 관리할 수 있습니다.
**쿠폰 등록 폼**은 다음 필드를 포함합니다.
* **Coupon Code** (필수, 영문 대문자 또는 숫자로만 구성, 6~12자, 중복 불가)
* **Discount Type** (드롭다운: `Fixed` / `Percent`)
* **Discount Value** (필수, 정수, 1 이상. `Percent` 선택 시 1~100 사이)
* **Minimum Order Amount** (필수, 정수, 0 이상. 이 금액 미만의 주문에는 적용 불가)
* **Expires At** (필수, `<input type="date">`. 오늘 날짜 이후만 선택 가능)
* **Usage Limit** (필수, 정수, 1 이상)
쿠폰 목록 테이블 컬럼: **Code**, **Type**, **Value**, **Min. Order**, **Expires At**, **Remaining Uses**, Delete button. 잔여 사용 횟수는 `remaining_uses` 컬럼으로 관리하며, 등록 시 입력한 Usage Limit 값으로 초기화하고 쿠폰이 사용될 때마다 1씩 차감합니다.
#### 2.5 주문 관리
주문 관리 페이지(`/admin/orders`)는 전체 주문 목록을 테이블로 표시합니다.
테이블 컬럼: **Order #**, **Customer**, **Phone**, **Total**, **Status**, **Date**.
주문 상태 기준으로 필터링하는 드롭다운을 제공하십시오. 상태값은 `Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`이며, 선택한 상태의 주문만 목록에 표시됩니다.
주문 상세 페이지(`/admin/orders/{id}`)에서는 다음 정보를 확인할 수 있어야 합니다.
* Customer Info: 이름(Name), 연락처(Phone), 배송 주소(Shipping Address)
* Order Items: 상품명(Product), 단가(Unit Price), 수량(Qty), 소계(Subtotal)
* Coupon: 적용된 쿠폰 코드 및 할인 금액. 쿠폰 미적용 시 **"None"** 표시
* Total: 최종 결제 금액
* **Order Status** 변경 셀렉트박스(`Pending` / `Processing` / `Shipped` / `Delivered` / `Cancelled`) 및 Save 버튼. 저장 성공 시 동일 페이지에 **"Order status updated."** 메시지를 표시합니다.
* 주문 상태가 **`Cancelled`**로 변경될 때 해당 주문에 포함된 상품의 재고를 주문 수량만큼 복원합니다. 이미 `Cancelled` 상태인 주문에서 다시 `Cancelled`로 변경하는 경우 재고 복원은 수행하지 않습니다.
---
### **3. 공개 페이지 (Public Pages)**
#### 3.1 홈 — 상품 목록
홈(`/`)은 상태가 **`Active`**인 상품 목록을 카드 형태로 표시합니다. 각 카드에는 대표 이미지(없는 경우 회색 placeholder 영역 표시), 상품명, 카테고리명, 가격이 표시됩니다.
한 페이지에 **12개**씩 표시하며 페이지네이션을 제공합니다. 최근 등록 순으로 정렬합니다.
카테고리 필터 링크를 제공하며, 특정 카테고리 클릭 시 해당 카테고리의 **`Active`** 상품만 표시됩니다. 페이지네이션은 현재 카테고리 필터를 유지합니다.
키워드 검색창을 제공하며, 상품명 기준으로 검색됩니다. 카테고리 필터와 검색이 동시에 적용됩니다. 검색 상태에서 페이지네이션도 검색 조건을 유지합니다.
#### 3.2 상품 상세
상품 상세 페이지는 `/products/{id}`에서 접근 가능합니다. 존재하지 않는 ID 또는 상태가 **`Hidden`**인 상품에 접근하면 404 페이지를 반환합니다.
상품 상세 페이지에는 대표 이미지(없는 경우 회색 placeholder 영역 표시), 상품명, 카테고리명, 가격, 재고 수량, 상품 설명이 표시됩니다.
재고가 0이거나 상태가 **`Out of Stock`**인 상품은 **"Out of Stock"** 표시와 함께 구매 버튼이 비활성화됩니다.
재고가 1 이상이고 상태가 **`Active`**인 상품에는 수량 입력(`<input type="number">`, 최솟값 1, 최댓값은 현재 재고 수량)과 **"Buy Now"** 버튼이 표시됩니다. 버튼 클릭 시 선택한 상품 ID와 수량을 쿼리스트링으로 전달하여 주문 접수 페이지(`/checkout?product_id={id}&quantity={n}`)로 이동합니다.
#### 3.3 주문 접수
주문 접수 페이지(`/checkout`)는 쿼리스트링의 `product_id``quantity`를 읽어 주문 대상 상품 정보(상품명, 단가, 수량, 소계)를 화면에 표시합니다. 서버에서 다음 조건을 확인하고, 하나라도 해당하면 홈(`/`)으로 리다이렉트합니다.
* `product_id`에 해당하는 상품이 존재하지 않는 경우
* 상품 상태가 **`Hidden`** 또는 **`Out of Stock`**인 경우
* 재고가 0인 경우
* `quantity`가 1 미만이거나 현재 재고를 초과하는 경우
구매자 이름(Name), 연락처(Phone), 배송 주소(Shipping Address)를 입력하는 폼이 있습니다. 모든 필드는 필수이며 **서버 측 유효성 검사**를 통과한 데이터만 주문이 접수됩니다.
**쿠폰 코드 입력란(Coupon Code)**과 **"Apply"** 버튼이 있습니다. "Apply" 버튼 클릭 시 `fetch`를 사용하여 `/checkout/coupon` 엔드포인트에 아래 형식으로 POST 요청을 보내고, 그 결과를 페이지 새로고침 없이 화면에 반영합니다. 해당 엔드포인트는 선수가 직접 구현해야 합니다.
* 요청 Body (JSON): `{ "coupon_code": "ABCD1234", "subtotal": 30000 }`
* 성공 응답 (JSON): `{ "valid": true, "discount_type": "fixed"|"percent", "discount_value": 3000, "discount_amount": 3000, "final_amount": 27000 }`
* 실패 응답 (JSON): `{ "valid": false, "message": "error message" }`
* 유효한 쿠폰: 할인 금액을 계산하여 소계(Subtotal), 할인 금액(Discount), 최종 결제 금액(Total)을 화면에 표시합니다.
* `Fixed` 할인: 소계에서 할인 값을 직접 차감합니다.
* `Percent` 할인: `소계 × 할인율 / 100`을 차감합니다. 소수점 이하는 버림(floor) 처리합니다.
* 최종 결제 금액은 0 미만이 되지 않습니다.
* 유효하지 않은 쿠폰: 각 상황에 맞는 오류 메시지를 표시합니다.
* 존재하지 않는 코드: **"Invalid coupon code."**
* 만료된 쿠폰: **"This coupon has expired."**
* 잔여 횟수 소진: **"This coupon has reached its usage limit."**
* 최소 주문금액 미달: **"This coupon requires a minimum order of {n}."**
주문 접수 시 다음을 **하나의 트랜잭션**으로 처리하십시오.
1. `orders` 테이블에 주문 정보(구매자명, 연락처, 배송주소, 적용된 쿠폰 ID — 없으면 NULL, 할인 금액 — 없으면 0, 최종 결제 금액)를 저장합니다.
2. `order_items` 테이블에 주문 상품 정보(주문 ID, 상품 ID, **상품명 스냅샷**, 수량, 단가)를 저장합니다. 상품명은 주문 시점의 이름을 그대로 저장하여 이후 상품이 삭제되거나 이름이 변경되어도 주문 내역에서 올바르게 표시되도록 합니다.
3. 해당 상품의 재고(`stock`)를 주문 수량만큼 차감합니다.
4. 쿠폰이 적용된 경우 해당 쿠폰의 `remaining_uses`를 1 차감합니다.
재고가 요청 수량보다 부족할 경우 트랜잭션을 롤백하고 **"Not enough stock."** 오류 메시지를 표시합니다.
주문 접수 성공 시 주문 완료 페이지(`/orders/{id}/complete`)로 이동합니다. 주문 완료 페이지는 별도 인증 없이 접근 가능하며, 주문번호(Order #), 주문 상품 내역, 할인 금액(Discount), 최종 결제 금액(Total)을 표시합니다.
#### 3.4 레이아웃
모든 공개 페이지에는 헤더와 푸터가 포함됩니다. 헤더에는 사이트명(**ShopPress**)과 홈으로 이동하는 링크가 포함됩니다. 각 페이지의 내용을 반영한 `<title>` 태그가 적절히 설정되어야 합니다.
관리자 패널 페이지에는 사이드바 또는 상단 내비게이션이 포함됩니다. 내비게이션 메뉴 항목: **Dashboard**, **Products**, **Categories**, **Coupons**, **Orders**.
---
## 4. 제출 안내 (Submission Guidelines)
선수는 작업 결과물을 서버 디렉토리 `/module_b/`에 업로드하십시오.
---
## 5. 채점 기준 요약 (Marking Scheme — Total 25점)
| 항목 | 세부 기준 | 배점 |
| :--- | :--- | :---: |
| **A. 인증** | 로그인/로그아웃, 세션 보호, 미인증 접근 리다이렉트 | 2 |
| **B. 대시보드** | 통계 4종 카드(영문 레이블), 최근 주문 5건 목록 | 1 |
| **C. 상품 관리** | 목록(검색 포함), 등록(이미지 선택), 수정(이미지 유지/교체+기존 삭제), 삭제(주문 이력 있는 경우 거부 + 이미지 파일 삭제) | 5 |
| **D. 카테고리 관리** | 추가(중복 검사), 수정, 삭제(참조 상품 존재 시 거부) | 2 |
| **E. 쿠폰 관리** | 쿠폰 등록(전체 유효성 검사 포함), 목록 조회(잔여 횟수 표시), 삭제 | 3 |
| **F. 주문 관리** | 목록(상태 필터), 상세 조회, 주문 상태 변경, Cancelled 시 재고 복원(중복 복원 방지) | 4 |
| **G. 공개 — 상품 목록** | Active 상품 필터링, 카테고리 필터, 키워드 검색, 페이지네이션(조건 유지) | 3 |
| **H. 공개 — 상품 상세** | 상품 정보 표시, Out of Stock 처리(재고 0 또는 Out of Stock 상태), 수량 입력 및 checkout 이동 | 1 |
| **I. 공개 — 주문 접수** | checkout 진입 유효성(Out of Stock/Hidden/재고 초과 포함), 쿠폰 적용(fetch + 할인 계산), 트랜잭션(주문저장+재고차감+쿠폰차감), 완료 페이지 | 3 |
| **J. 유효성 검사** | 서버 측 이미지 용량/확장자 검사, 주문 폼 필드 검사 | 1 |
| **합계** | | **25** |