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

[5기] 4주차 쇼핑몰 과제 제출 - 치즈 #8

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# spring-shopping-product
# 기능 목록
## 상품
- [x] 상품을 추가할 수 있다.
- [x] 상품 목록을 조회할 수 있다.
- [x] 상품 상세 정보를 조회할 수 있다.
- [x] 상품을 수정할 수 있다.
- [x] 상품을 삭제할 수 있다.
- [x] 상품 이미지는 URL로 저장한다.
- [x] 상품 이름은 공백 포함 15자까지 입력할 수 있다.
- [x] 상품 이름의 특수 문자는 (), [], +, -, &, /, _만 입력할 수 있다.
- [x] 상품 이름은 비속어를 포함할 수 없다.
- [PurgoMalum](https://www.purgomalum.com/)에서 욕설이 포함되어 있는지 확인한다.
## 회원
- [x] 회원 가입을 할 수 있다.
- 이메일과 비밀번호를 입력하여 가입한다.
- [x] 회원은 로그인할 수 있다.
## 위시 리스트
- [ ] 위시 리스트에 상품을 추가할 수 있다.
- [ ] 위시 리스트에 등록된 상품 목록을 조회할 수 있다.
- [ ] 위시 리스트에 담긴 상품을 삭제할 수 있다.
23 changes: 10 additions & 13 deletions build.gradle.kts → build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
plugins {
id("org.springframework.boot") version "3.3.1"
id("io.spring.dependency-management") version "1.1.5"
kotlin("plugin.jpa") version "1.9.24"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24"
id "java"
}

group = "camp.nextstep.edu"
Expand All @@ -24,23 +22,22 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("io.rest-assured:rest-assured:5.5.0")
// lombok
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
// crypto
implementation("org.springframework.security:spring-security-crypto")
// jwt
implementation("io.jsonwebtoken:jjwt:0.12.6")
}

kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}

tasks.withType<Test> {
tasks.named("test") {
useJUnitPlatform()
}
14 changes: 14 additions & 0 deletions src/main/java/shopping/config/RestClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package shopping.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

@Bean
public RestClient restClient() {
return RestClient.create();
}
}
15 changes: 15 additions & 0 deletions src/main/java/shopping/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package shopping.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
23 changes: 23 additions & 0 deletions src/main/java/shopping/controller/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package shopping.controller;

import lombok.RequiredArgsConstructor;
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;
import shopping.dto.LoginRequestDto;
import shopping.dto.LoginResponseDto;
import shopping.service.LoginService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/login")
public class LoginController {
private final LoginService loginService;

@PostMapping
public ResponseEntity<LoginResponseDto> login(@RequestBody LoginRequestDto request) {
return ResponseEntity.ok(loginService.login(request.getEmail(), request.getPassword()));
}
}
26 changes: 26 additions & 0 deletions src/main/java/shopping/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package shopping.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import shopping.entity.Member;
import shopping.service.MemberService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

private final MemberService memberService;

@PostMapping
public ResponseEntity<Member> join(@Valid @RequestBody Member member) {
try {
return ResponseEntity.ok(memberService.join(member));
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}
}
55 changes: 55 additions & 0 deletions src/main/java/shopping/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package shopping.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import shopping.service.ProductService;
import shopping.entity.Product;

import java.util.List;

@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {

private final ProductService productService;

@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<Product> addProduct(@Valid @RequestBody Product product) {
try {
return ResponseEntity.ok(productService.addProduct(product));
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}

@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id, @Valid @RequestBody Product productDetails) {
try {
return ResponseEntity.ok(productService.updateProduct(id, productDetails));
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
13 changes: 13 additions & 0 deletions src/main/java/shopping/dto/LoginRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package shopping.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class LoginRequestDto {
private String email;
private String password;
}
12 changes: 12 additions & 0 deletions src/main/java/shopping/dto/LoginResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package shopping.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class LoginResponseDto {
private String accessToken;
}
29 changes: 29 additions & 0 deletions src/main/java/shopping/entity/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package shopping.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import org.springframework.security.crypto.password.PasswordEncoder;

@Entity
@Getter
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Email(message = "Invalid email format")
private String email;

@NotBlank
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;

public void encodePassword(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
}
}
28 changes: 28 additions & 0 deletions src/main/java/shopping/entity/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package shopping.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;

@Entity
@Getter
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Size(max = 15)
@Pattern(regexp = "^[\\w\\s\\-\\&\\(\\)\\[\\]/+가-힣]+$", message = "Invalid product name")
private String name;

private String imageUrl;

public void update(Product productDetails) {
this.name = productDetails.getName();
this.imageUrl = productDetails.getImageUrl();
}
}
29 changes: 29 additions & 0 deletions src/main/java/shopping/entity/ShoppingToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package shopping.entity;

import lombok.Getter;

import java.util.Objects;

@Getter
public class ShoppingToken {
private final Long id;
private final String email;

public ShoppingToken(final Long id, final String email) {
this.id = id;
this.email = email;
}

@Override
public boolean equals(final Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
final ShoppingToken shoppingToken = (ShoppingToken) object;
return Objects.equals(id, shoppingToken.id) && Objects.equals(email, shoppingToken.email);
}

@Override
public int hashCode() {
return Objects.hash(id, email);
}
}
27 changes: 27 additions & 0 deletions src/main/java/shopping/entity/UserDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package shopping.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UserDetail {
private Long id;
private String email;
private String password;

public Long getId() {
return id;
}

public String getEmail() {
return email;
}

public boolean isWrongPassword(String password, PasswordEncoder passwordEncoder) {
return !passwordEncoder.matches(password, this.password);
}
}
20 changes: 20 additions & 0 deletions src/main/java/shopping/infra/ProfanityChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package shopping.infra;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
@RequiredArgsConstructor
public class ProfanityChecker {

private final RestClient restClient;

public boolean hasProfanity(String text) {
final String response = restClient.get()
.uri("https://www.purgomalum.com/service/containsprofanity?text={text}", text)
.retrieve()
.body(String.class);
return Boolean.parseBoolean(response);
}
}
Loading