Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 BE_문성민 1주차 (3단계) #222

Open
wants to merge 46 commits into
base: dalsungmin
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ccfe6d6
docs: 구현할 기능 목록 및 API
Dalsungmin Jun 25, 2024
6e6ec39
feat: 상품 정보를 저장할 Product 클래스 생
Dalsungmin Jun 25, 2024
9794a14
fix: 패키지 다시 설정
Dalsungmin Jun 25, 2024
7fe78e4
fix: 문제 조건대로 자료형 수정
Dalsungmin Jun 25, 2024
86b5f04
feat: 기본 상품 추가
Dalsungmin Jun 25, 2024
190fc0b
feat: GET으로 전체상품 조회하기
Dalsungmin Jun 25, 2024
8c419a5
feat: POST로 상품 목록에 추가하기
Dalsungmin Jun 25, 2024
f87feba
feat: PUT으로 상품 수정하기
Dalsungmin Jun 25, 2024
be225fb
feat: Delete로 상품 삭제하기
Dalsungmin Jun 25, 2024
6984f31
feat: GET으로 특정 상품만 조회하기
Dalsungmin Jun 25, 2024
8eec380
feat: API 확인
Dalsungmin Jun 25, 2024
2ec0bcb
fix: 다른 기능에서의 예외 처리 추가
Dalsungmin Jun 26, 2024
04e5f33
style: 컨트롤러 수정
Dalsungmin Jun 27, 2024
3a7e1c0
feat: 상품 목록 조
Dalsungmin Jun 27, 2024
b43278b
feat: 상품 추가
Dalsungmin Jun 27, 2024
58012c5
feat: 상품 수정
Dalsungmin Jun 27, 2024
f193ebc
feat: 상품 삭제
Dalsungmin Jun 27, 2024
f8fdd7d
fix: 필요없는 코드 정리
Dalsungmin Jun 27, 2024
4280926
feat: 관리자 화면 이름'Manage Products' 설정
Dalsungmin Jun 27, 2024
0d8334d
feat: 새 상품을 추가하는 버튼 생성
Dalsungmin Jun 27, 2024
3dfdd2c
feat: 상품 목록 테이블
Dalsungmin Jun 27, 2024
735479e
feat: 상품 추가, 수정 모달
Dalsungmin Jun 27, 2024
a7374f6
feat: Java Script 코드
Dalsungmin Jun 27, 2024
109bf78
feat: 프로퍼티 파일 수정
Dalsungmin Jun 28, 2024
f214d86
feat: SQL 스크립트 파일 생성
Dalsungmin Jun 28, 2024
37f382b
feat: Product 엔티티 클래스
Dalsungmin Jun 28, 2024
b265c2b
feat: 생성자 설정
Dalsungmin Jun 28, 2024
0b1731b
feat: 모든 상품 조회 기능 구현
Dalsungmin Jun 28, 2024
3ea32e7
feat: 특정 상품 조회 기능 구현
Dalsungmin Jun 28, 2024
c322714
feat: 새로운 상품 저장 기능 구현
Dalsungmin Jun 28, 2024
2a00fcc
feat: 상품 수정 기능 구현
Dalsungmin Jun 28, 2024
f2719b6
feat: 상품 삭제 기능 구현
Dalsungmin Jun 28, 2024
a2da338
feat: ProductRepository를 주입
Dalsungmin Jun 28, 2024
5147fd8
feat: 모든 상품 목록 조회 후 전달
Dalsungmin Jun 28, 2024
c1a303f
feat: 새로운 상품을 데이터베이스에 추가
Dalsungmin Jun 28, 2024
01ee909
feat: 기존 상품 수정
Dalsungmin Jun 28, 2024
f97352b
feat: 지정된 ID의 상품을 삭제
Dalsungmin Jun 28, 2024
3d8bacc
feat: 상품 상세 정보 조회
Dalsungmin Jun 28, 2024
d421418
feat: 특정 상품 조회
Dalsungmin Jun 28, 2024
729d5cc
feat: 상품 검색해서 조회하는 버튼 추가
Dalsungmin Jun 28, 2024
7e2b2a4
fix: 영어 버튼 한글로 변경
Dalsungmin Jun 28, 2024
a2a57be
feat: 상품마다 상세 조회 버튼 추가
Dalsungmin Jun 28, 2024
163dbd0
feat: 존재하지 않는 ID 입력시 에러메시지 출력
Dalsungmin Jun 28, 2024
9fb6ca7
feat: 상세 정보를 표시하는 페이지 생성
Dalsungmin Jun 28, 2024
f1966dd
README 업데이트
Dalsungmin Jun 28, 2024
1dbbb3e
Merge branch 'dalsungmin' into step3
azjaehyun Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* HTTP 요청과 응답은 JSON 형식으로 주고받는다.
* 현재는 별도의 데이터베이스가 없으므로 적절한 자바 컬렉션 프레임워크를 사용하여 메모리에 저장한다.


# step1
## 상품 조회 API
### Request
GET /api/products HTTP/1.1
Expand Down Expand Up @@ -70,3 +72,18 @@ DELETE /api/products/1 HTTP/1.1

### Response
HTTP/1.1 200

# step2
* 상품을 조회, 추가, 수정, 삭제할 수 있는 관리자 화면을 구현한다.
* Thymeleaf를 사용하여 서버 사이드 렌더링을 구현한다.
* 기본적으로는 HTML 폼 전송 등을 이용한 페이지 이동을 기반으로 하지만, 자바스크립트를 이용한 비동기 작업에 관심이 있다면 이미 만든 상품 API를 이용하여 AJAX 등의 방식을 적용할 수 있다.
* 상품 이미지의 경우, 파일을 업로드하지 않고 URL을 직접 입력한다.
* 상품을 추가하는 버튼을 누르면 모달창이 뜨면서 상세 정보를 입력하도록 생성

# step3
* 자바 컬렉션 프레임워크를 사용하여 메모리에 저장하던 상품 정보를 데이터베이스에 저장한다.
* 메모리에 저장하고 있던 모든 코드를 제거하고 H2 데이터베이스를 사용하도록 변경한다.
* 사용하는 테이블은 애플리케이션이 실행될 때 구축되어야 한다.
* step2 에서 사용했던 html에서 id를 이용해 특정 상품을 검색 할 수 있는 버튼 추가
* 각 상품마다도 버튼을 누르면 상세정보를 보여주는 페이지로 넘어가도록 구현
* 상세정보를 보여주는 product-detail.html 추가
21 changes: 16 additions & 5 deletions src/main/java/gift/Product.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package gift;

public class Product {
private long id;
private Long id;
private String name;
private int price;
private String imageUrl;

// Getters, Setters
public long getId() {
// 기본 생성자
public Product() {
}

// 매개변수가 있는 생성자
public Product(Long id, String name, int price, String imageUrl) {
this.id = id;
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}

// Getter와 Setter 메서드
public Long getId() {
return id;
}

public void setId(long id) {
public void setId(Long id) {
this.id = id;
}

Expand All @@ -38,5 +50,4 @@ public String getImageUrl() {
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}

}
77 changes: 46 additions & 31 deletions src/main/java/gift/ProductController.java
Original file line number Diff line number Diff line change
@@ -1,61 +1,76 @@
package gift;

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/products")
public class ProductController {
private final Map<Long, Product> products = new HashMap<>();
private long nextId = 1L;

@PostConstruct
public void init() {
Product product1 = new Product();
product1.setId(8146027);
product1.setName("아이스 카페 아메리카노 T");
product1.setPrice(4500);
product1.setImageUrl("https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg");
products.put(product1.getId(), product1);
private final ProductRepository productRepository;

public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}

@GetMapping
public String getProducts(Model model) {
model.addAttribute("products", new ArrayList<>(products.values()));
model.addAttribute("product", new Product()); // Add empty product object for the form
model.addAttribute("products", productRepository.findAll());
model.addAttribute("product", new Product());
return "product-list";
}

@PostMapping
public String addProduct(@ModelAttribute Product product) {
product.setId(nextId++);
products.put(product.getId(), product);
public String addProduct(@ModelAttribute Product product, RedirectAttributes redirectAttributes) {
try {
productRepository.save(product);
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error adding product: " + e.getMessage());
}
return "redirect:/products";
}

@PostMapping("/{id}")
public String updateProduct(@PathVariable Long id, @ModelAttribute Product product) {
if (!products.containsKey(id)) {
throw new IllegalArgumentException("상품이 없습니다.");
public String updateProduct(@PathVariable Long id, @ModelAttribute Product product, RedirectAttributes redirectAttributes) {
try {
product.setId(id);
productRepository.update(product);
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error updating product: " + e.getMessage());
}
product.setId(id);
products.put(id, product);
return "redirect:/products";
}

@PostMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id) {
if (!products.containsKey(id)) {
throw new IllegalArgumentException("상품이 없습니다.");
public String deleteProduct(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {
productRepository.deleteById(id);
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error deleting product: " + e.getMessage());
}
products.remove(id);
return "redirect:/products";
}

}
@GetMapping("/view/{id}")
public String getProductDetails(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) {
try {
Product product = productRepository.findById(id);
model.addAttribute("product", product);
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
return "redirect:/products";
}
return "product-detail";
}

@GetMapping("/{id}")
@ResponseBody
public Product getProductById(@PathVariable("id") Long id) {
try {
return productRepository.findById(id);
} catch (Exception e) {
throw new IllegalArgumentException("Product not found: " + e.getMessage());
}
}
}
64 changes: 64 additions & 0 deletions src/main/java/gift/ProductRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gift;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

//import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class ProductRepository {
private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert simpleJdbcInsert;

public ProductRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("products")
.usingGeneratedKeyColumns("id");
}

public List<Product> findAll() {
return jdbcTemplate.query("SELECT * FROM products", (rs, rowNum) -> {
Product product = new Product();
product.setId(rs.getLong("id"));
product.setName(rs.getString("name"));
product.setPrice(rs.getInt("price"));
product.setImageUrl(rs.getString("image_url"));
return product;
});
}

public Product findById(Long id) {
return jdbcTemplate.queryForObject("SELECT * FROM products WHERE id = ?", new Object[]{id}, (rs, rowNum) -> {
Product product = new Product();
product.setId(rs.getLong("id"));
product.setName(rs.getString("name"));
product.setPrice(rs.getInt("price"));
product.setImageUrl(rs.getString("image_url"));
return product;
});
}

public Product save(Product product) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", product.getName());
parameters.put("price", product.getPrice());
parameters.put("image_url", product.getImageUrl());
Number newId = simpleJdbcInsert.executeAndReturnKey(parameters);
product.setId(newId.longValue());
return product;
}

public void update(Product product) {
jdbcTemplate.update("UPDATE products SET name = ?, price = ?, image_url = ? WHERE id = ?",
product.getName(), product.getPrice(), product.getImageUrl(), product.getId());
}

public void deleteById(Long id) {
jdbcTemplate.update("DELETE FROM products WHERE id = ?", id);
}
}
9 changes: 9 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
spring.application.name=spring-gift
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
1 change: 1 addition & 0 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO products (name, price, image_url) VALUES ('아이스 카페 아메리카노 T', 4500, 'https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg');
6 changes: 6 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price INT NOT NULL,
image_url VARCHAR(255)
);
41 changes: 41 additions & 0 deletions src/main/resources/templates/product-detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Product Details</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1 class="my-4">Product Details</h1>

<div th:if="${errorMessage}" class="alert alert-danger" role="alert">
<p th:text="${errorMessage}"></p>
</div>

<table class="table table-bordered">
<tr>
<th>ID</th>
<td th:text="${product.id}"></td>
</tr>
<tr>
<th>Name</th>
<td th:text="${product.name}"></td>
</tr>
<tr>
<th>Price</th>
<td th:text="${product.price}"></td>
</tr>
<tr>
<th>Image</th>
<td><img th:src="${product.imageUrl}" alt="Product Image" style="width:200px;height:auto;"/></td>
</tr>
</table>

<a href="/products" class="btn btn-secondary">Back to Products</a>
</div>

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
39 changes: 35 additions & 4 deletions src/main/resources/templates/product-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
<h1 class="my-4">Manage <strong>Products</strong></h1>

<div class="mb-4">
<button class="btn btn-success" data-toggle="modal" data-target="#productModal">Add New Product</button>
<button class="btn btn-success" data-toggle="modal" data-target="#productModal">상품 추가</button>
<input type="text" id="productId" placeholder="Enter Product ID" class="form-control d-inline-block w-auto ml-2">
<button class="btn btn-info" onclick="searchProductById()">상품 조회</button>
</div>

<div th:if="${errorMessage}" class="alert alert-danger" role="alert">
<p th:text="${errorMessage}"></p>
</div>

<table class="table table-striped">
Expand All @@ -29,10 +35,11 @@ <h1 class="my-4">Manage <strong>Products</strong></h1>
<td th:text="${product.price}"></td>
<td><img th:src="${product.imageUrl}" alt="Product Image" style="width:100px;height:auto;"/></td>
<td>
<button class="btn btn-warning" th:onclick="'editProduct(' + ${product.id} + ')'">Edit</button>
<button class="btn btn-warning" th:onclick="'editProduct(' + ${product.id} + ')'">수정</button>
<form th:action="@{/products/delete/{id}(id=${product.id})}" method="post" style="display:inline;">
<button type="submit" class="btn btn-danger">Delete</button>
<button type="submit" class="btn btn-danger">삭제</button>
</form>
<a th:href="@{/products/view/{id}(id=${product.id})}" class="btn btn-info">조회</a>
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -80,15 +87,39 @@ <h5 class="modal-title" id="productModalLabel">Product Form</h5>
<script>
function editProduct(id) {
fetch(`/products/${id}`)
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error('존재하지 않는 상품 입니다.');
}
return response.json();
})
.then(product => {
document.getElementById('name').value = product.name;
document.getElementById('price').value = product.price;
document.getElementById('imageUrl').value = product.imageUrl;
document.getElementById('productForm').action = `/products/${id}`;
$('#productModal').modal('show');
})
.catch(error => {
alert(error.message);
});
}

function searchProductById() {
const productId = document.getElementById('productId').value;
if (productId) {
fetch(`/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error('존재하지 않는 상품 입니다.');
}
window.location.href = `/products/view/${productId}`;
})
.catch(error => {
alert(error.message);
});
}
}
</script>
</body>
</html>