Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- Thymeleaf + Spring Security 통합 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/kr/ac/hansung/DataInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,25 @@ public void run(ApplicationArguments args) {
productRepository.save(new Product("Spring Security 7 핵심 원리", 28000, "세션·JWT·OAuth2 기반 보안 구현", 30));
productRepository.save(new Product("JPA 프로그래밍 실전", 32000, "Hibernate 7 기반 ORM 마스터", 25));
productRepository.save(new Product("Thymeleaf 완전 정복", 22000, "서버사이드 템플릿 엔진 가이드", 40));
log.info("샘플 상품 4건 생성 완료");
productRepository.save(new Product("React 입문", 27000, "컴포넌트 기반 프론트엔드 개발", 0));
productRepository.save(new Product("Docker & Kubernetes", 38000, "컨테이너 오케스트레이션 실전", 15));
productRepository.save(new Product("Git & GitHub 협업", 18000, "브랜치 전략과 Pull Request 가이드", 60));
productRepository.save(new Product("MySQL 성능 최적화", 33000, "인덱스·쿼리 튜닝 실전 가이드", 20));
productRepository.save(new Product("Redis 캐싱 전략", 29000, "캐시 설계와 세션 관리 실습", 10));
productRepository.save(new Product("AWS 클라우드 입문", 42000, "EC2·S3·RDS 실전 배포 가이드", 35));

productRepository.save(new Product("삼성전자 갤럭시 S25", 1290000, "최신 플래그십 스마트폰", 100));
productRepository.save(new Product("삼성전자 갤럭시 탭 S10", 890000, "고해상도 AMOLED 태블릿", 45));
productRepository.save(new Product("LG 그램 17 노트북", 1890000, "초경량 17인치 업무용 노트북", 8));
productRepository.save(new Product("애플 맥북 프로 M4", 2990000, "M4 칩 탑재 고성능 노트북", 0));
productRepository.save(new Product("소니 WH-1000XM6", 420000, "업계 최고 노이즈 캔슬링 헤드폰", 22));
productRepository.save(new Product("로지텍 MX Master 3S", 129000, "무선 고정밀 업무용 마우스", 75));
productRepository.save(new Product("삼성전자 65인치 QLED TV", 1590000, "Neo QLED 4K 스마트 TV", 5));
productRepository.save(new Product("다이슨 V15 청소기", 890000, "레이저 먼지 감지 무선 청소기", 18));
productRepository.save(new Product("닌텐도 스위치 2", 459000, "휴대·거치 겸용 게임 콘솔", 0));
productRepository.save(new Product("애플 에어팟 프로 3", 359000, "공간 음향 지원 무선 이어폰", 55));

log.info("샘플 상품 20건 생성 완료");
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/kr/ac/hansung/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/", "/login", "/signup",
"/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/products/add", "/products/*/delete").hasRole("ADMIN")
.requestMatchers("/products/add", "/products/*/delete", "/products/*/edit").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/products").hasRole("ADMIN")
.anyRequest().authenticated()
)
Expand Down
64 changes: 56 additions & 8 deletions src/main/java/kr/ac/hansung/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package kr.ac.hansung.controller;

import kr.ac.hansung.dto.ProductDto;
import kr.ac.hansung.entity.Product;
import kr.ac.hansung.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/products")
Expand All @@ -15,15 +20,27 @@ public class ProductController {
private final ProductService productService;

@GetMapping
public String list(Model model) {
model.addAttribute("products", productService.findAll());
return "products/list";
}
public String list(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "5") int size,
@RequestParam(required = false) String keyword,
Model model) {

@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
model.addAttribute("product", productService.findById(id));
return "products/detail";
PageRequest pageRequest = PageRequest.of(page, size, Sort.by("id"));

String normalizedKeyword = (keyword != null && !keyword.isBlank()) ? keyword : null;

Page<Product> productPage;

if (normalizedKeyword != null) {
productPage = productService.searchProducts(normalizedKeyword, pageRequest);
} else {
productPage = productService.getProducts(pageRequest);
}

model.addAttribute("productPage", productPage);
model.addAttribute("keyword", normalizedKeyword);

return "products/list";
}

@GetMapping("/add")
Expand All @@ -38,6 +55,37 @@ public String save(@ModelAttribute ProductDto dto) {
return "redirect:/products";
}

@GetMapping("/{id}/edit")
public String editForm(@PathVariable Long id, Model model) {
Product product = productService.findById(id);

ProductDto dto = new ProductDto();
dto.setName(product.getName());
dto.setPrice(product.getPrice());
dto.setDescription(product.getDescription());
dto.setStock(product.getStock());

model.addAttribute("product", dto);
model.addAttribute("productId", id);

return "products/edit";
}

@PostMapping("/{id}/edit")
public String edit(@PathVariable Long id,
@ModelAttribute ProductDto dto,
RedirectAttributes ra) {
productService.updateProduct(id, dto);
ra.addFlashAttribute("successMessage", "상품이 수정되었습니다.");
return "redirect:/products";
}

@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
model.addAttribute("product", productService.findById(id));
return "products/detail";
}

@PostMapping("/{id}/delete")
public String delete(@PathVariable Long id) {
productService.deleteById(id);
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/kr/ac/hansung/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package kr.ac.hansung.controller;

import jakarta.validation.Valid;
import kr.ac.hansung.dto.PasswordChangeDto;
import kr.ac.hansung.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequiredArgsConstructor
public class UserController {

private final UserService userService;

@GetMapping("/user/password")
public String passwordForm(org.springframework.ui.Model model) {
model.addAttribute("passwordChangeDto", new PasswordChangeDto());
return "user/password";
}

@PostMapping("/user/password")
public String changePassword(
@AuthenticationPrincipal UserDetails userDetails,
@Valid @ModelAttribute("passwordChangeDto") PasswordChangeDto dto,
BindingResult bindingResult,
RedirectAttributes ra) {

if (bindingResult.hasErrors()) {
return "user/password";
}

if (!dto.getNewPassword().equals(dto.getConfirmPassword())) {
bindingResult.rejectValue(
"confirmPassword",
"mismatch",
"새 비밀번호가 일치하지 않습니다"
);
return "user/password";
}

try {
userService.changePassword(
userDetails.getUsername(),
dto.getCurrentPassword(),
dto.getNewPassword()
);

ra.addFlashAttribute(
"successMessage",
"비밀번호가 변경되었습니다"
);

} catch (IllegalArgumentException e) {

bindingResult.rejectValue(
"currentPassword",
"wrong",
e.getMessage()
);

return "user/password";
}

return "redirect:/home";
}
}
21 changes: 21 additions & 0 deletions src/main/java/kr/ac/hansung/dto/PasswordChangeDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kr.ac.hansung.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PasswordChangeDto {

@NotBlank(message = "현재 비밀번호를 입력하세요")
private String currentPassword;

@NotBlank
@Size(min = 8, message = "새 비밀번호는 8자 이상이어야 합니다")
private String newPassword;

@NotBlank(message = "새 비밀번호 확인을 입력하세요")
private String confirmPassword;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package kr.ac.hansung.repository;

import kr.ac.hansung.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long> {
long countByStockEquals(int stock);
}

@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
Page<Product> findByNameContaining(@Param("keyword") String keyword, Pageable pageable);
}
24 changes: 24 additions & 0 deletions src/main/java/kr/ac/hansung/service/ProductService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import kr.ac.hansung.entity.Product;
import kr.ac.hansung.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -21,6 +23,16 @@ public List<Product> findAll() {
return productRepository.findAll();
}

@Transactional(readOnly = true)
public Page<Product> getProducts(Pageable pageable) {
return productRepository.findAll(pageable);
}

@Transactional(readOnly = true)
public Page<Product> searchProducts(String keyword, Pageable pageable) {
return productRepository.findByNameContaining(keyword, pageable);
}

@Transactional(readOnly = true)
public Product findById(Long id) {
return productRepository.findById(id)
Expand All @@ -39,4 +51,16 @@ public Product save(ProductDto dto) {
public void deleteById(Long id) {
productRepository.deleteById(id);
}

@Transactional
public Product updateProduct(Long id, ProductDto dto) {
Product product = findById(id);

product.setName(dto.getName());
product.setPrice(dto.getPrice());
product.setDescription(dto.getDescription());
product.setStock(dto.getStock());

return product;
}
}
17 changes: 17 additions & 0 deletions src/main/java/kr/ac/hansung/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import kr.ac.hansung.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -38,4 +39,20 @@ public void signup(UserDto dto) {
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}

@Transactional
public void changePassword(String email,
String currentPassword,
String newPassword) {

User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException(email));

if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
throw new IllegalArgumentException("현재 비밀번호가 일치하지 않습니다");
}

user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
}
3 changes: 3 additions & 0 deletions src/main/resources/templates/admin/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,8 @@ <h6 class="fw-bold">접속 계정</h6>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<footer style="text-align:center; margin-top:20px;">
<p th:text="${'실습 수행: ' + #temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss') + ' | 학번: 2171284 | 성명: 하지현'}"></p>
</footer>
</body>
</html>
4 changes: 4 additions & 0 deletions src/main/resources/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark mb-4">
<div class="container">
<div th:if="${successMessage}" class="alert alert-success" th:text="${successMessage}"></div>
<a class="navbar-brand" th:href="@{/home}">HanSung</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu">
<span class="navbar-toggler-icon"></span>
Expand Down Expand Up @@ -82,5 +83,8 @@ <h6 class="fw-bold">관리자 대시보드</h6>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<footer style="text-align:center; margin-top:20px;">
<p th:text="${'실습 수행: ' + #temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss') + ' | 학번: 2171284 | 성명: 하지현'}"></p>
</footer>
</body>
</html>
3 changes: 3 additions & 0 deletions src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@
</div>
</div>
</div>
<footer style="text-align:center; margin-top:20px;">
<p th:text="${'실습 수행: ' + #temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss') + ' | 학번: 2171284 | 성명: 하지현'}"></p>
</footer>
</body>
</html>
3 changes: 3 additions & 0 deletions src/main/resources/templates/products/add.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,8 @@ <h4 class="fw-bold mb-0">상품 등록</h4>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<footer style="text-align:center; margin-top:20px;">
<p th:text="${'실습 수행: ' + #temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss') + ' | 학번: 2171284 | 성명: 하지현'}"></p>
</footer>
</body>
</html>
3 changes: 3 additions & 0 deletions src/main/resources/templates/products/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,8 @@ <h3 class="fw-bold mb-1" th:text="${product.name}"></h3>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<footer style="text-align:center; margin-top:20px;">
<p th:text="${'실습 수행: ' + #temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss') + ' | 학번: 2171284 | 성명: 하지현'}"></p>
</footer>
</body>
</html>
Loading