# 🛍️ 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** |
