diff --git a/README.md b/README.md index 789090c3e..031ca85ab 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ ## 1단계 요구사항 - 유효성 검사 및 예외 처리 - 상품을 추가하거나 수정할 때 -- [] 공백을 포함하여 최대 15자까지 입력하였는지 유효성을 검증한다. -- [] 가능한 특수문자(( ), [ ], +, -, &, /, _)를 사용하였는지 검증한다. +- [x] 공백을 포함하여 최대 15자까지 입력하였는지 유효성을 검증한다. +- [x] 가능한 특수문자(( ), [ ], +, -, &, /, _)를 사용하였는지 검증한다. +- [x] "카카오"가 포함된 문구는 담당 MD와 협의한 경우에만 사용할 수 있다. (이를 클라이언트에게 적절히 알리기 + +## 2단계 요구사항 - 회원 가입 +- [x] 회원은 이메일과 비밀번호를 입력하여 가입한다. +- [x] 토큰을 받으려면 이메일과 비밀번호를 보내야 하며, 가입한 이메일과 비밀번호가 일치하면 토큰이 발급된다. +- [x] 토큰을 생성하는 방법에는 여러 가지가 있다. 방법 중 하나를 선택한다. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 575a63230..cef7d5fcc 100644 --- a/build.gradle +++ b/build.gradle @@ -18,14 +18,17 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + runtimeOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.springframework.boot:spring-boot-starter-validation' - } tasks.named('test') { diff --git a/src/main/java/gift/config/DatabaseInitializer.java b/src/main/java/gift/config/DatabaseInitializer.java index c0d3263d8..d610803f4 100644 --- a/src/main/java/gift/config/DatabaseInitializer.java +++ b/src/main/java/gift/config/DatabaseInitializer.java @@ -25,5 +25,13 @@ public void initialize() { System.out.println("Executing SQL: " + sql); jdbcTemplate.execute(sql); System.out.println("Table kakaoProduct created successfully"); + + String sql2 = "CREATE TABLE IF NOT EXISTS members (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + + "email VARCHAR(255) NOT NULL, " + + "password VARCHAR(255) NOT NULL)"; + System.out.println("Executing SQL: " + sql2); + jdbcTemplate.execute(sql2); + System.out.println("Table members created successfully"); } } diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java new file mode 100644 index 000000000..3ce7cf71d --- /dev/null +++ b/src/main/java/gift/controller/MemberController.java @@ -0,0 +1,31 @@ +package gift.controller; + +import gift.domain.Member; +import gift.service.MemberService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/members") +public class MemberController { + private final MemberService memberService; + + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody Member member) { + memberService.register(member); + return ResponseEntity.ok("User registered successfully"); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Member member) { + String token = memberService.login(member.getEmail(), member.getPassword()); + return ResponseEntity.ok(token); + } +} diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java index d12d51cfd..a18073d41 100644 --- a/src/main/java/gift/controller/ProductController.java +++ b/src/main/java/gift/controller/ProductController.java @@ -30,10 +30,15 @@ public String newProductForm(Model model) { } @PostMapping - public String createProduct(@Valid @ModelAttribute Product product, BindingResult bindingResult) { + public String createProduct(@Valid @ModelAttribute Product product, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "productForm"; } + // "카카오" 문구 포함 여부 확인하는 로직 추가 + if (product.getName().contains("카카오")) { + model.addAttribute("errorMessage", "담당 MD와 협의된 경우에만 '카카오'를 포함할 수 있습니다."); + return "productForm"; + } productRepository.save(product); return "redirect:/api/products"; } @@ -46,10 +51,15 @@ public String editProductForm(@PathVariable Long id, Model model) { } @PostMapping("/edit/{id}") - public String updateProduct(@PathVariable Long id, @Valid @ModelAttribute Product updatedProduct, BindingResult bindingResult) { + public String updateProduct(@PathVariable Long id, @Valid @ModelAttribute Product updatedProduct, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "productForm"; } + // "카카오" 문구 포함 여부 확인 + if (updatedProduct.getName().contains("카카오")) { + model.addAttribute("errorMessage", "담당 MD와 협의된 경우에만 '카카오'를 포함할 수 있습니다."); + return "productForm"; + } updatedProduct.setId(id); productRepository.update(updatedProduct); return "redirect:/api/products"; diff --git a/src/main/java/gift/domain/Member.java b/src/main/java/gift/domain/Member.java new file mode 100644 index 000000000..6ec9c801a --- /dev/null +++ b/src/main/java/gift/domain/Member.java @@ -0,0 +1,39 @@ +package gift.domain; + +public class Member { + private Long id; + private String email; + private String password; + + public Member() {} + + public Member(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} + diff --git a/src/main/java/gift/domain/Product.java b/src/main/java/gift/domain/Product.java index cb69bbf5b..f5a0bb09e 100644 --- a/src/main/java/gift/domain/Product.java +++ b/src/main/java/gift/domain/Product.java @@ -9,7 +9,7 @@ public class Product { @NotBlank(message = "Name cannot be blank") @Size(max = 15, message = "Name cannot be longer than 15 characters") - @Pattern(regexp = "^[\\w\\s\\(\\)\\[\\]+\\-&/_]*$", message = "Name contains invalid characters") // 정규표현식을 통한 특수 문자 제어 + @Pattern(regexp = "^[ㄱ-ㅎ가-힣a-zA-Z0-9\\s\\(\\)\\[\\]+\\-&/_]*$", message = "이름에 유효하지 않은 문자가 포함되어 있습니다") private String name; private int price; private String imageUrl; diff --git a/src/main/java/gift/repository/MemberRepository.java b/src/main/java/gift/repository/MemberRepository.java new file mode 100644 index 000000000..b7e8332b3 --- /dev/null +++ b/src/main/java/gift/repository/MemberRepository.java @@ -0,0 +1,48 @@ +package gift.repository; + +import gift.domain.Member; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +@Repository +public class MemberRepository { + private final JdbcTemplate jdbcTemplate; + + public MemberRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void save(Member member) { + String sql = "INSERT INTO members (email, password) VALUES (?, ?)"; + jdbcTemplate.update(sql, member.getEmail(), member.getPassword()); + } + + public Optional findByEmail(String email) { + String sql = "SELECT * FROM members WHERE email = ?"; + try { + Member member = jdbcTemplate.queryForObject(sql, new Object[]{email}, new MemberRowMapper()); + return Optional.ofNullable(member); + } catch (Exception e) { + return Optional.empty(); + } + } + + private static class MemberRowMapper implements RowMapper { + @Override + public Member mapRow(ResultSet rs, int rowNum) throws SQLException { + Member member = new Member(); + member.setId(rs.getLong("id")); + member.setEmail(rs.getString("email")); + member.setPassword(rs.getString("password")); + return member; + } + } +} + + + diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java new file mode 100644 index 000000000..595cf7735 --- /dev/null +++ b/src/main/java/gift/service/MemberService.java @@ -0,0 +1,45 @@ +package gift.service; + +import gift.domain.Member; +import gift.repository.MemberRepository; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.Optional; + +@Service +public class MemberService { + private final MemberRepository memberRepository; + @Value("${jwt.secret}") + private String secretKey; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public void register(Member member) { + memberRepository.save(member); + } + + public String login(String email, String password) { + Optional member = memberRepository.findByEmail(email); + if (member.isPresent() && member.get().getPassword().equals(password)) { + return generateToken(member.get()); + } else { + throw new RuntimeException("Invalid email or password"); + } + } + + private String generateToken(Member member) { + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("email", member.getEmail()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour + .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) + .compact(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..a59face3c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ 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= +spring.h2.console.enabled=true +jwt.secret=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= diff --git a/src/main/resources/templates/productForm.html b/src/main/resources/templates/productForm.html index 24fbe345c..3b23cf36c 100644 --- a/src/main/resources/templates/productForm.html +++ b/src/main/resources/templates/productForm.html @@ -22,6 +22,8 @@

Product Form

Image URL Error
+ +
Error Message
Cancel