From 9d1b6e29e87f12e3b4f956584bbcd31d824381ac Mon Sep 17 00:00:00 2001 From: jihyeon Date: Sun, 7 Jun 2026 17:21:03 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8A=B5=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=91=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/admin/dashboard.html | 3 +++ src/main/resources/templates/home.html | 3 +++ src/main/resources/templates/login.html | 3 +++ src/main/resources/templates/products/add.html | 3 +++ src/main/resources/templates/products/detail.html | 3 +++ src/main/resources/templates/products/list.html | 3 +++ src/main/resources/templates/signup.html | 3 +++ 7 files changed, 21 insertions(+) diff --git a/src/main/resources/templates/admin/dashboard.html b/src/main/resources/templates/admin/dashboard.html index c062c3b..ccddbdc 100644 --- a/src/main/resources/templates/admin/dashboard.html +++ b/src/main/resources/templates/admin/dashboard.html @@ -96,5 +96,8 @@
접속 계정
+ diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index d6ce33a..33f7dde 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -82,5 +82,8 @@
관리자 대시보드
+ diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index ce94274..f788117 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -59,5 +59,8 @@ + diff --git a/src/main/resources/templates/products/add.html b/src/main/resources/templates/products/add.html index a72b0ec..76b2233 100644 --- a/src/main/resources/templates/products/add.html +++ b/src/main/resources/templates/products/add.html @@ -93,5 +93,8 @@

상품 등록

+ diff --git a/src/main/resources/templates/products/detail.html b/src/main/resources/templates/products/detail.html index 100ca14..70cfb32 100644 --- a/src/main/resources/templates/products/detail.html +++ b/src/main/resources/templates/products/detail.html @@ -70,5 +70,8 @@

+ diff --git a/src/main/resources/templates/products/list.html b/src/main/resources/templates/products/list.html index 194756e..3bcbc0c 100644 --- a/src/main/resources/templates/products/list.html +++ b/src/main/resources/templates/products/list.html @@ -107,5 +107,8 @@

상품 목록

+ diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html index 4b74101..66849af 100644 --- a/src/main/resources/templates/signup.html +++ b/src/main/resources/templates/signup.html @@ -52,5 +52,8 @@ + From e771563361ff2a31dd2b8815ad27d5a513d44b1d Mon Sep 17 00:00:00 2001 From: jihyeon Date: Sun, 7 Jun 2026 17:24:07 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=83=98=ED=94=8C=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/ac/hansung/DataInitializer.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/hansung/DataInitializer.java b/src/main/java/kr/ac/hansung/DataInitializer.java index f31ff36..c5a6ad3 100644 --- a/src/main/java/kr/ac/hansung/DataInitializer.java +++ b/src/main/java/kr/ac/hansung/DataInitializer.java @@ -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건 생성 완료"); } } } From cdbf4d134946d7a47ae5b5e9a3e1b96a4235b7ac Mon Sep 17 00:00:00 2001 From: jihyeon Date: Sun, 7 Jun 2026 17:36:30 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=EC=83=81=ED=92=88=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20&=20=EA=B2=80=EC=83=89=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hansung/controller/ProductController.java | 26 +++++++++- .../hansung/repository/ProductRepository.java | 9 +++- .../kr/ac/hansung/service/ProductService.java | 12 +++++ .../resources/templates/products/list.html | 47 +++++++++++++++++-- 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/main/java/kr/ac/hansung/controller/ProductController.java b/src/main/java/kr/ac/hansung/controller/ProductController.java index 3009fc2..7f1b3cf 100644 --- a/src/main/java/kr/ac/hansung/controller/ProductController.java +++ b/src/main/java/kr/ac/hansung/controller/ProductController.java @@ -1,8 +1,12 @@ 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.*; @@ -15,8 +19,26 @@ public class ProductController { private final ProductService productService; @GetMapping - public String list(Model model) { - model.addAttribute("products", productService.findAll()); + public String list(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "5") int size, + @RequestParam(required = false) String keyword, + Model model) { + + PageRequest pageRequest = PageRequest.of(page, size, Sort.by("id")); + + String normalizedKeyword = (keyword != null && !keyword.isBlank()) ? keyword : null; + + Page 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"; } diff --git a/src/main/java/kr/ac/hansung/repository/ProductRepository.java b/src/main/java/kr/ac/hansung/repository/ProductRepository.java index f171d47..589a7d5 100644 --- a/src/main/java/kr/ac/hansung/repository/ProductRepository.java +++ b/src/main/java/kr/ac/hansung/repository/ProductRepository.java @@ -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 { long countByStockEquals(int stock); -} + + @Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%") + Page findByNameContaining(@Param("keyword") String keyword, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/kr/ac/hansung/service/ProductService.java b/src/main/java/kr/ac/hansung/service/ProductService.java index 35c7f05..175dbed 100644 --- a/src/main/java/kr/ac/hansung/service/ProductService.java +++ b/src/main/java/kr/ac/hansung/service/ProductService.java @@ -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; @@ -21,6 +23,16 @@ public List findAll() { return productRepository.findAll(); } + @Transactional(readOnly = true) + public Page getProducts(Pageable pageable) { + return productRepository.findAll(pageable); + } + + @Transactional(readOnly = true) + public Page searchProducts(String keyword, Pageable pageable) { + return productRepository.findByNameContaining(keyword, pageable); + } + @Transactional(readOnly = true) public Product findById(Long id) { return productRepository.findById(id) diff --git a/src/main/resources/templates/products/list.html b/src/main/resources/templates/products/list.html index 3bcbc0c..7060eb8 100644 --- a/src/main/resources/templates/products/list.html +++ b/src/main/resources/templates/products/list.html @@ -48,18 +48,30 @@
+ 상품 등록
+
+ + + 초기화 +
+
-
+

등록된 상품이 없습니다.

-
+
@@ -73,7 +85,7 @@

상품 목록

- +
상품 목록
+
From 7909509c38c5f7fdedf2161c0e911d4c69328658 Mon Sep 17 00:00:00 2001 From: jihyeon Date: Sun, 7 Jun 2026 17:54:24 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/hansung/config/SecurityConfig.java | 2 +- .../hansung/controller/ProductController.java | 38 +++++++++++++--- .../kr/ac/hansung/service/ProductService.java | 12 ++++++ .../resources/templates/products/edit.html | 43 +++++++++++++++++++ .../resources/templates/products/list.html | 2 + 5 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/main/resources/templates/products/edit.html diff --git a/src/main/java/kr/ac/hansung/config/SecurityConfig.java b/src/main/java/kr/ac/hansung/config/SecurityConfig.java index bc04eca..606b62a 100644 --- a/src/main/java/kr/ac/hansung/config/SecurityConfig.java +++ b/src/main/java/kr/ac/hansung/config/SecurityConfig.java @@ -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() ) diff --git a/src/main/java/kr/ac/hansung/controller/ProductController.java b/src/main/java/kr/ac/hansung/controller/ProductController.java index 7f1b3cf..4f03291 100644 --- a/src/main/java/kr/ac/hansung/controller/ProductController.java +++ b/src/main/java/kr/ac/hansung/controller/ProductController.java @@ -10,6 +10,7 @@ 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") @@ -42,12 +43,6 @@ public String list(@RequestParam(defaultValue = "0") int page, return "products/list"; } - @GetMapping("/{id}") - public String detail(@PathVariable Long id, Model model) { - model.addAttribute("product", productService.findById(id)); - return "products/detail"; - } - @GetMapping("/add") public String addForm(Model model) { model.addAttribute("product", new ProductDto()); @@ -60,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); diff --git a/src/main/java/kr/ac/hansung/service/ProductService.java b/src/main/java/kr/ac/hansung/service/ProductService.java index 175dbed..adecd53 100644 --- a/src/main/java/kr/ac/hansung/service/ProductService.java +++ b/src/main/java/kr/ac/hansung/service/ProductService.java @@ -51,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; + } } diff --git a/src/main/resources/templates/products/edit.html b/src/main/resources/templates/products/edit.html new file mode 100644 index 0000000..0370646 --- /dev/null +++ b/src/main/resources/templates/products/edit.html @@ -0,0 +1,43 @@ + + + + + 상품 수정 + + + + +
+

상품 수정

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + 취소 +
+
+ +
+

+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/products/list.html b/src/main/resources/templates/products/list.html index 7060eb8..d6fed8b 100644 --- a/src/main/resources/templates/products/list.html +++ b/src/main/resources/templates/products/list.html @@ -105,6 +105,8 @@

상품 목록

+ 수정 +
From f0cb43da9cd1829116e0ca57c94f12b72729e967 Mon Sep 17 00:00:00 2001 From: jihyeon Date: Sun, 7 Jun 2026 18:07:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 ++ .../ac/hansung/controller/UserController.java | 71 +++++++++++++++++++ .../kr/ac/hansung/dto/PasswordChangeDto.java | 21 ++++++ .../kr/ac/hansung/service/UserService.java | 17 +++++ src/main/resources/templates/home.html | 1 + .../resources/templates/user/password.html | 45 ++++++++++++ 6 files changed, 160 insertions(+) create mode 100644 src/main/java/kr/ac/hansung/controller/UserController.java create mode 100644 src/main/java/kr/ac/hansung/dto/PasswordChangeDto.java create mode 100644 src/main/resources/templates/user/password.html diff --git a/pom.xml b/pom.xml index 70c79e5..719d894 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,11 @@ spring-boot-starter-thymeleaf + + org.springframework.boot + spring-boot-starter-validation + + org.thymeleaf.extras diff --git a/src/main/java/kr/ac/hansung/controller/UserController.java b/src/main/java/kr/ac/hansung/controller/UserController.java new file mode 100644 index 0000000..d63254e --- /dev/null +++ b/src/main/java/kr/ac/hansung/controller/UserController.java @@ -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"; + } +} \ No newline at end of file diff --git a/src/main/java/kr/ac/hansung/dto/PasswordChangeDto.java b/src/main/java/kr/ac/hansung/dto/PasswordChangeDto.java new file mode 100644 index 0000000..394d0d9 --- /dev/null +++ b/src/main/java/kr/ac/hansung/dto/PasswordChangeDto.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/kr/ac/hansung/service/UserService.java b/src/main/java/kr/ac/hansung/service/UserService.java index 13cdfb5..9e55a0a 100644 --- a/src/main/java/kr/ac/hansung/service/UserService.java +++ b/src/main/java/kr/ac/hansung/service/UserService.java @@ -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; @@ -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); + } } diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 33f7dde..b0d8088 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -20,6 +20,7 @@