diff --git a/.gitignore b/.gitignore index c2065bc..1e97e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +**/src/main/resources/*.properties +**/src/main/resources/*.yml ### STS ### .apt_generated diff --git a/.gitignore.swp b/.gitignore.swp new file mode 100644 index 0000000..5380ce2 Binary files /dev/null and b/.gitignore.swp differ diff --git a/build.gradle b/build.gradle index 15b77ef..6000719 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.0.1' - id 'io.spring.dependency-management' version '1.1.0' + id 'org.springframework.boot' version '2.7.8' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' } -group = 'com.dku' +group = 'com.cha' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { @@ -15,15 +15,24 @@ configurations { } repositories { - mavenCentral() + mavenCentral(); } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'io.springfox:springfox-swagger2:2.9.2' + implementation 'io.springfox:springfox-swagger-ui:2.9.2' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + testImplementation 'junit:junit:4.13.1' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/settings.gradle b/settings.gradle index 73e37fd..9d00a1e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'springstudy' +rootProject.name = 'carrotApi' diff --git a/src/main/java/com/cha/carrotApi/CarrotApiApplication.java b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java new file mode 100644 index 0000000..8f5cdfd --- /dev/null +++ b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java @@ -0,0 +1,14 @@ +package com.cha.carrotApi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class CarrotApiApplication { + public static void main(String[] args) { + SpringApplication.run(CarrotApiApplication.class, args); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java new file mode 100644 index 0000000..721e6c3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java @@ -0,0 +1,24 @@ +package com.cha.carrotApi.DTO.category; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +@ApiModel(value = "카테고리 생성 요청 처리") +@AllArgsConstructor +@NoArgsConstructor +public class CategoryCreateRequest { + @ApiModelProperty(value = "카테고리 명", notes = "카테고리 명을 입력하세요.", required = true, example = "category 1") + @NotBlank(message = "카테고리 명을 입력하세요.") + @Size(min = 2, max = 15, message = "길이 제한은 2~15자 이내입니다.") + private String name; + + @ApiModelProperty(value = "부모 카테고리 id", notes = "부모 카테고리의 id를 입력하세요.") + private int parentId; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java new file mode 100644 index 0000000..fc680b4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java @@ -0,0 +1,28 @@ +package com.cha.carrotApi.DTO.category; + +import com.cha.carrotApi.domain.Category.Category; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class CategoryDto { + private int id; + private String name; + private List children; + + public static List toDtoList(List categories) { + CategoryHelper helper = CategoryHelper.newInstance( + categories, + c -> new CategoryDto(c.getId(), c.getName(), new ArrayList<>()), + c -> c.getParent(), + c -> c.getId(), + d -> d.getChildren()); + return helper.convert(); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java new file mode 100644 index 0000000..62f10f4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java @@ -0,0 +1,78 @@ +package com.cha.carrotApi.DTO.category; + +import com.cha.carrotApi.exception.CannotConvertHelperException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class CategoryHelper { + private List entities; + private Function toDto; + private Function getParent; + private Function getKey; + private Function> getChildren; + + public static CategoryHelper newInstance(List entities, Function toDto, Function getParent, + Function getKey, Function> getChildren) { + return new CategoryHelper(entities, toDto, getParent, getKey, getChildren); + } + + private CategoryHelper(List entities, Function toDto, Function getParent, Function getKey, + Function> getChildren) { + this.entities = entities; + this.toDto = toDto; + this.getParent = getParent; + this.getKey = getKey; + this.getChildren = getChildren; + } + + public List convert() { + try { + return convertInternal(); + } catch (NullPointerException e) { + throw new CannotConvertHelperException(e.getMessage()); + } + } + + private List convertInternal() { + Map map = new HashMap<>(); + List roots = new ArrayList<>(); + + for (E e : entities) { + D dto = toDto(e); + map.put(getKey(e), dto); + if (hasParent(e)) { + E parent = getParent(e); + K parentKey = getKey(parent); + D parentDto = map.get(parentKey); + getChildren(parentDto).add(dto); + } else { + roots.add(dto); + } + } + return roots; + } + + private boolean hasParent(E e) { + return getParent(e) != null; + } + + private E getParent(E e) { + return getParent.apply(e); + } + + private D toDto(E e) { + return toDto.apply(e); + } + + private K getKey(E e) { + return getKey.apply(e); + } + + private List getChildren(D d) { + return getChildren.apply(d); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/exception/BaseResponse.java b/src/main/java/com/cha/carrotApi/DTO/exception/BaseResponse.java new file mode 100644 index 0000000..d224107 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/exception/BaseResponse.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.DTO.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BaseResponse { + private boolean isSuccess; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/exception/ErrorResponse.java b/src/main/java/com/cha/carrotApi/DTO/exception/ErrorResponse.java new file mode 100644 index 0000000..4ec2d24 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/exception/ErrorResponse.java @@ -0,0 +1,29 @@ +package com.cha.carrotApi.DTO.exception; + +import com.cha.carrotApi.exception.ErrorCode; +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class ErrorResponse { + private final LocalDateTime timestamp = LocalDateTime.now(); + private final int status; + private final String error; + private final String code; + private final String message; + + public static ResponseEntity toResponseEntity(ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(ErrorResponse.builder() + .status(errorCode.getHttpStatus().value()) + .error(errorCode.getHttpStatus().name()) + .message(errorCode.getMessage()) + .build() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/cha/carrotApi/DTO/exception/SuccessResponse.java b/src/main/java/com/cha/carrotApi/DTO/exception/SuccessResponse.java new file mode 100644 index 0000000..b2352b3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/exception/SuccessResponse.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi.DTO.exception; + +import lombok.Getter; +import org.springframework.lang.Nullable; + +@Getter +public class SuccessResponse extends BaseResponse{ + + private T data; + + public SuccessResponse(@Nullable T data) { + super(true); + this.data = data; + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java b/src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java new file mode 100644 index 0000000..c8961d5 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java @@ -0,0 +1,17 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Image; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ImageDto{ + private int id; + private String originName; + private String uniqueName; + + public static ImageDto toDto(Image image) { + return new ImageDto(image.getId(), image.getOriginName(), image.getUniqueName()); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java new file mode 100644 index 0000000..c017e94 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java @@ -0,0 +1,24 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Post; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PageInfoDto { + private int totalPage; + private int nowPage; + private int numberOfElements; + private boolean isNext; + + public PageInfoDto(Page result) { + this.totalPage = result.getTotalPages(); + this.nowPage = result.getNumber(); + this.numberOfElements = result.getNumberOfElements(); + this.isNext = result.hasNext(); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java new file mode 100644 index 0000000..8aa5f2c --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java @@ -0,0 +1,29 @@ +package com.cha.carrotApi.DTO.post; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiOperation(value = "게시글 생성 요청") +public class PostCreateRequest { + @ApiModelProperty(value = "게시글 제목", notes = "게시글 제목을 입력하세요.", required = true, example = "게시글 제목") + @NotBlank(message = "게시글 제목을 입력하세요.") + private String title; + + @ApiModelProperty(value = "게시글 내용", notes = "게시글 내용을 입력하세요.", required = true, example = "게시글 내용") + @NotBlank(message = "게시글 내용을 입력하세요.") + private String content; + + @ApiModelProperty(value = "이미지", notes = "이미지를 첨부해주세요.") + private List images = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java new file mode 100644 index 0000000..a456259 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@AllArgsConstructor +@NotBlank +public class PostCreateResponse { + private Long id; + private String title; + private String content; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java b/src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java new file mode 100644 index 0000000..d5745e1 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java @@ -0,0 +1,19 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PostFindAll { + private List posts; + private PageInfoDto pageInfoDto; + + public static PostFindAll toDto(List posts, PageInfoDto pageInfoDto){ + return new PostFindAll(posts, pageInfoDto); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java new file mode 100644 index 0000000..ecbbf0a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java @@ -0,0 +1,20 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Data +@AllArgsConstructor +public class PostListDto { + private Integer totalElement; + private Integer totalPages; + private boolean hasNext; + private List postList; + + public static PostListDto toDto(Page page) { + return new PostListDto(page.getTotalPages(), (int)page.getTotalElements(), page.hasNext(), page.getContent()); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java b/src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java new file mode 100644 index 0000000..4fc7dd3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java @@ -0,0 +1,26 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import javax.validation.constraints.PositiveOrZero; +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostReadCondition { + @NotNull(message = "페이지 번호를 입력하세요.") + @PositiveOrZero(message = "올바른 페이지 번호를 입력해주세요. (0 이상)") + private int page; + + @NotNull(message = "페이지 크기를 입력하세요.") + @Positive(message = "올바른 페이지 크기를 입력하세요. (1 이상)") + private int size; + + private List userId = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java new file mode 100644 index 0000000..6dc8ea6 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java @@ -0,0 +1,37 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Post; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostResponseDto { + private Long id; + private String writer_nickname; + private String title; + private String content; + private int likeCount; + private int interested; + private List images; + private LocalDateTime createdAt; + + public static PostResponseDto toDto(Post post, String writer_nickname) { + return new PostResponseDto( + post.getId(), + writer_nickname, + post.getTitle(), + post.getContent(), + post.getLikeCount(), + post.getInterested(), + post.getImages().stream().map(i -> ImageDto.toDto(i)).collect(Collectors.toList()), + post.getCreateDate() + ); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java new file mode 100644 index 0000000..17971cb --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java @@ -0,0 +1,22 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Post; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostSimpleDto { + private Long id; + private String title; + private String content; + private int liked; + private int interested; + + public PostSimpleDto toDto(Post post) { + return new PostSimpleDto(post.getId(), post.getTitle(), post.getUser().getNickname(), post.getLikeCount(), + post.getInterested()); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java b/src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java new file mode 100644 index 0000000..19e1b98 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java @@ -0,0 +1,32 @@ +package com.cha.carrotApi.DTO.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +@ApiModel(value = "게시글 수정") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostUpdateRequest { + @ApiModelProperty(value = "게시글 제목", notes = "게시글 제목을 입력해주세요.") + @NotBlank(message = "게시글 제목을 입력해주세요.") + private String title; + + @ApiModelProperty(value = "게시글 내용", notes = "게시글 내용을 입력해주세요.") + @NotBlank(message = "게시글 내용을 입력해주세요.") + private String content; + + @ApiModelProperty(value = "추가된 이미지", notes = "추가된 이미지를 첨부해주세요.") + private List addedImages = new ArrayList<>(); + + @ApiModelProperty(value = "제거된 이미지 아이디", notes = "제거된 이미지 아이디를 입력해주세요.") + private List deletedImages = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/DTO/user/LoginRequest.java b/src/main/java/com/cha/carrotApi/DTO/user/LoginRequest.java new file mode 100644 index 0000000..3055fd5 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/user/LoginRequest.java @@ -0,0 +1,20 @@ +package com.cha.carrotApi.DTO.user; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Data +@AllArgsConstructor +public class LoginRequest { + @Email(message = "이메일 형식에 맞지 않습니다.") + @NotBlank(message = "이메일을 입력해주세요.") + private String email; + + @NotBlank(message = "비밀번호를 입력해주세요") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\\\d)(?=.*[@$!%*#?&])[A-Za-z\\\\d@$!%*#?&]{8,30}$") + private String password; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java b/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java new file mode 100644 index 0000000..ed43a8d --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java @@ -0,0 +1,50 @@ +package com.cha.carrotApi.DTO.user; + +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.domain.User.Role; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +import javax.validation.constraints.*; + +@Data +@Getter +@Builder +@AllArgsConstructor +public class SignUpRequest { + + protected SignUpRequest(){} + @Email(message = "이메일 형식에 맞지 않습니다.") + @NotBlank(message = "아이디를 입력해주세요.") + private String email; + + @NotBlank(message = "닉네임을 입력해주세요") + @Size(min=2, message = "닉네임이 너무 짧습니다.") + private String nickname; + + @NotBlank(message = "비밀번호를 입력해주세요") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,30}$", + message = "비밀번호는 8~30 자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.") + private String password; + + @NotBlank(message = "핸드폰 번호를 입력해주세요.") + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", + message = "핸드폰 번호는 010-xxxx-xxxx 형식으로 입력해주세요.") + private String phonenumber; + + + private Role role; + + @Builder + public User toEntity() { + return User.builder() + .email(email) + .nickname(nickname) + .password(password) + .phonenumber(phonenumber) + .role(Role.USER) + .build(); + } +} diff --git a/src/main/java/com/cha/carrotApi/ERD/carrotERD.png b/src/main/java/com/cha/carrotApi/ERD/carrotERD.png new file mode 100644 index 0000000..6abf398 Binary files /dev/null and b/src/main/java/com/cha/carrotApi/ERD/carrotERD.png differ diff --git a/src/main/java/com/cha/carrotApi/controller/CategoryController.java b/src/main/java/com/cha/carrotApi/controller/CategoryController.java new file mode 100644 index 0000000..058ba20 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/CategoryController.java @@ -0,0 +1,51 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.DTO.category.CategoryCreateRequest; +import com.cha.carrotApi.response.Response; +import com.cha.carrotApi.service.Category.CategoryService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@Api(value = "Category Controller", tags = "Category") +@RequiredArgsConstructor +@RestController +public class CategoryController { + private final CategoryService categoryService; + + @ApiOperation(value = "모든 카테고리 조회", notes = "모든 카테고리를 조회합니다.") + @GetMapping("/categories") + @ResponseStatus(HttpStatus.OK) + public Response findAllCategories() { + return Response.success(categoryService.findAllCategory()); + } + + @ApiOperation(value = "카테고리 생성", notes = "카테고리를 생성합니다.") + @PostMapping("/categories") + @ResponseStatus(HttpStatus.CREATED) + public Response createCategory(@Valid @RequestBody CategoryCreateRequest req) { + categoryService.createCategory(req); + return Response.success(); + } + +// @ApiOperation(value = "카테고리 첫 생성", notes = "카테고리를 처음 생성합니다.") +// @PostMapping("/categories/start") +// @ResponseStatus(HttpStatus.CREATED) +// public Response createCategoryAtFirst() { +// categoryService.createAtFirst(); +// return Response.success(); +// } + + @ApiOperation(value = "카테고리 삭제", notes = "카테고리를 삭제합니다.") + @DeleteMapping("/categories/{id}") + @ResponseStatus(HttpStatus.OK) + public Response deleteCategory(@ApiParam(value = "카테고리 id", required = true) @PathVariable int id) { + categoryService.deleteCategory(id); + return Response.success(); + } +} diff --git a/src/main/java/com/cha/carrotApi/controller/PostController.java b/src/main/java/com/cha/carrotApi/controller/PostController.java new file mode 100644 index 0000000..29ae5b9 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/PostController.java @@ -0,0 +1,104 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.DTO.post.PostCreateRequest; +import com.cha.carrotApi.DTO.post.PostUpdateRequest; +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.exception.UserNotFoundException; +import com.cha.carrotApi.repository.User.UserRepository; +import com.cha.carrotApi.response.Response; +import com.cha.carrotApi.service.Post.PostService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@Slf4j +@Api(value = "Post Controller", tags = "Post") +@RequiredArgsConstructor +public class PostController { + private final PostService postService; + private final UserRepository userRepository; + + @ApiOperation(value = "글 생성", notes = "글을 작성합니다.") + @PostMapping("/posts") + @ResponseStatus(HttpStatus.CREATED) + public Response createBoard(@Valid @ModelAttribute PostCreateRequest req, + @RequestParam(value = "category", defaultValue = "1") int categoryId) { + // http://localhost:8080/boards?category=3 + User user = getPrincipal(); + return Response.success(postService.createPost(req, categoryId, user)); + } + + @ApiOperation(value = "게시글 목록 조회", notes = "게시글 목록을 조회합니다.") + @GetMapping("/posts/all/{categoryId}") + @ResponseStatus(HttpStatus.OK) + public Response findAllPosts(@ApiParam(value = "카테고리 id", required = true) @PathVariable int categoryId, @RequestParam(defaultValue = "0") Integer page) { + // http://localhost:8080/boards/all/{categoryId}?page=0 + return Response.success(postService.findAllPosts(page, categoryId)); + } + + @ApiOperation(value = "게시글 수정", notes = "게시글을 수정합니다.") + @PutMapping("/posts/{id}") + @ResponseStatus(HttpStatus.OK) + public Response editBoard(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id, + @Valid @ModelAttribute PostUpdateRequest req) { + User user = getPrincipal(); + return Response.success(postService.editPost(id, req, user)); + } + + @ApiOperation(value = "게시글 좋아요", notes = "사용자가 게시물 좋아요를 누릅니다.") + @PostMapping("/posts/{id}") + @ResponseStatus(HttpStatus.OK) + public Response likeBoard(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) { + User user = getPrincipal(); + return Response.success(postService.updateLikeOfPost(id, user)); + } + + @ApiOperation(value = "게시글 관심품목", notes = "사용자가 게시물을 관심품목에 등록합니다.") + @PostMapping("posts/{id}/favorites") + @ResponseStatus(HttpStatus.OK) + public Response favoritePost(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) { + User user = getPrincipal(); + return Response.success(postService.updateInterestedPost(id, user)); + } + + @ApiOperation(value = "게시글 삭제", notes = "게시글을 삭제합니다.") + @DeleteMapping("/posts/{id}") + @ResponseStatus(HttpStatus.OK) + public Response deleteBoard(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) { + User user = getPrincipal(); + postService.deletePost(id, user); + return Response.success(); + } + + @ApiOperation(value = "게시글 검색", notes = "게시글을 검색합니다.") + @GetMapping("/posts/search") + @ResponseStatus(HttpStatus.OK) + public Response searchPost(String keyword, + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC)Pageable pageable) { + // ex) http://localhost:8080/api/boards/search?page=0 + return Response.success(postService.searchPost(keyword, pageable)); + } + + + + + private User getPrincipal() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user = userRepository.findByNickname(authentication.getName()) + .orElseThrow(UserNotFoundException::new); + return user; + } +} diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java new file mode 100644 index 0000000..b033095 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -0,0 +1,31 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.DTO.user.SignUpRequest; +import com.cha.carrotApi.service.User.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Map; + +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + //회원가입 + @PostMapping("/join") + @ResponseStatus(HttpStatus.OK) + public Long join(@Valid @RequestBody SignUpRequest request) throws Exception { + return userService.signUp(request); + } + + //로그인 + @PostMapping("/login") + public String login(@RequestBody Map user) { + return userService.login(user); + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java b/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java new file mode 100644 index 0000000..190e868 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java @@ -0,0 +1,25 @@ +package com.cha.carrotApi.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + + @Column(name = "CREATEDATE") + @CreatedDate + private LocalDateTime createDate; + + @Column(name = "MODIFIEDDATE") + @LastModifiedDate + private LocalDateTime modifiedDate; +} diff --git a/src/main/java/com/cha/carrotApi/domain/Category/Category.java b/src/main/java/com/cha/carrotApi/domain/Category/Category.java new file mode 100644 index 0000000..6edeefe --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Category/Category.java @@ -0,0 +1,34 @@ +package com.cha.carrotApi.domain.Category; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "CATEGORY") +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "category_id") + private int id; + + @Column(length = 30, nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private Category parent; + + public Category(String name, Category parent) { + this.name = name; + this.parent = parent; + } +} + diff --git a/src/main/java/com/cha/carrotApi/domain/Post/Image.java b/src/main/java/com/cha/carrotApi/domain/Post/Image.java new file mode 100644 index 0000000..1361e16 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/Image.java @@ -0,0 +1,57 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.exception.UnsupportedImageFormatException; +import lombok.*; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.UUID; + +@Entity +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Image extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false) + private String uniqueName; + + @Column(nullable = false) + private String originName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "POST_ID", nullable = false) + private Post post; + + private final static String supportedExtension[] = {"jpg", "jpeg", "gif", "bmp", "png"}; + + public Image(String originName) { + this.originName = originName; + this.uniqueName = generateUniqueName(extractExtension(originName)); + } + + public void initPost(Post post) { + if(this.post == null){ + this.post = post; + } + } + + private String generateUniqueName(String extension) { + return UUID.randomUUID().toString() + "." + extension; + } + + private String extractExtension(String originName) { + try { + String ext = originName.substring(originName.lastIndexOf(".") + 1); + if(isSupportedFormat(ext)) return ext; + } catch (StringIndexOutOfBoundsException e) { } + throw new UnsupportedImageFormatException(); + } + + private boolean isSupportedFormat(String ext) { + return Arrays.stream(supportedExtension).anyMatch(e -> e.equalsIgnoreCase(ext)); + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java b/src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java new file mode 100644 index 0000000..cbcfcdf --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java @@ -0,0 +1,34 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.User.User; +import lombok.*; + +import javax.persistence.*; + +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Entity +public class InterestedPost extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "POST_ID", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID", nullable = false) + private User user; + + @Column(nullable = false) + private boolean status; + + public InterestedPost(Post post, User user) { + this.post = post; + this.user = user; + this.status = true; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/LikePost.java b/src/main/java/com/cha/carrotApi/domain/Post/LikePost.java new file mode 100644 index 0000000..900b57f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/LikePost.java @@ -0,0 +1,35 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.User.User; +import lombok.*; + +import javax.persistence.*; + +@Entity +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter @Setter +@Builder +public class LikePost extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "POST_ID", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID", nullable = false) + private User user; + + @Column(nullable = false) + private boolean status; + //true = 좋아요 + + public LikePost(Post post, User user) { + this.post = post; + this.user = user; + this.status = true; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/Post.java b/src/main/java/com/cha/carrotApi/domain/Post/Post.java new file mode 100644 index 0000000..e6f8387 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/Post.java @@ -0,0 +1,139 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.DTO.post.PostUpdateRequest; +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.Category.Category; +import com.cha.carrotApi.domain.User.User; +import lombok.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.*; + +@Getter @Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "POST") +public class Post extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "POST_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID") + private User user; + + @Column(name = "TITLE") + private String title; + + @Column(name = "content") + @Lob + private String content; + + @Column(name = "LIKE_COUNT") + private int likeCount; + + @Column(name = "INTEREST_COUNT") + private int interested; + + @Column(name = "POST_STATUS") + @Enumerated(EnumType.STRING) + private PostStatus postStatus; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id", nullable = false) + private Category category; + +// @ManyToMany +// @JoinTable(name = "INTEREST_POST", +// joinColumns = @JoinColumn(name = "POST_ID"), +// inverseJoinColumns = @JoinColumn(name = "USER_ID")) +// private List interest_posts = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) + private List images; + + public Post(User user, String title, String content, Category category, List images) { + this.user = user; + this.title = title; + this.content = content; + this.likeCount = 0; + this.interested = 0; + this.postStatus = PostStatus.ITEM_SELLING; + this.category = category; + this.images = new ArrayList<>(); + addImages(images); + } + + + public ImageUpdatedResult update(PostUpdateRequest req) { + this.title = title; + this.content = content; + ImageUpdatedResult result = findImageUpdatedResult(req.getAddedImages(), req.getDeletedImages()); + addImages(result.getAddedImages()); + deleteImages(result.getDeletedImages()); + return result; + } + + private void deleteImages(List deleted) { + deleted.stream().forEach(di -> this.images.remove(di)); + } + + private void addImages(List added) { + added.stream().forEach(i -> { + images.add(i); + i.initPost(this); + }); + } + + private ImageUpdatedResult findImageUpdatedResult(List addedImagesFiles, List deletedImagesIds) { + List addedImages = convertImageFilesToImages(addedImagesFiles); + List deletedImages = convertImageIdsToImages(deletedImagesIds); + return new ImageUpdatedResult(addedImagesFiles, addedImages, deletedImages); + } + + private List convertImageIdsToImages(List imageIds) { + return imageIds.stream() + .map(id -> convertImageIdToImages(id)) + .filter(i -> i.isPresent()) + .map(i -> i.get()) + .collect(toList()); + } + + private Optional convertImageIdToImages(int id) { + return this.images.stream().filter(i -> i.getId() == (id)).findAny(); + } + + + private List convertImageFilesToImages(List imageFiles) { + return imageFiles.stream().map(imageFile -> new Image(imageFile.getOriginalFilename())).collect(toList()); + } + + @Getter + @AllArgsConstructor + public static class ImageUpdatedResult { + private List addedImageFiles; + private List addedImages; + private List deletedImages; + } + + public void increaseLikeCount() { + this.likeCount += 1; + } + public void decreaseLikeCount() { + this.likeCount -= 1; + } + + public void increaseInterested() { + this.interested += 1; + } + public void decreaseInterested() { + this.interested -= 1; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java b/src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java new file mode 100644 index 0000000..2c12792 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java @@ -0,0 +1,5 @@ +package com.cha.carrotApi.domain.Post; + +public enum PostStatus { + ITEM_SELLING, ITEM_RESERVED, ITEM_SOLD +} diff --git a/src/main/java/com/cha/carrotApi/domain/User/Role.java b/src/main/java/com/cha/carrotApi/domain/User/Role.java new file mode 100644 index 0000000..ab862c0 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/User/Role.java @@ -0,0 +1,5 @@ +package com.cha.carrotApi.domain.User; + +public enum Role { + USER, MANAGER, ADMIN; +} diff --git a/src/main/java/com/cha/carrotApi/domain/User/User.java b/src/main/java/com/cha/carrotApi/domain/User/User.java new file mode 100644 index 0000000..b816d97 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/User/User.java @@ -0,0 +1,63 @@ +package com.cha.carrotApi.domain.User; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.Post.Post; +import lombok.*; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Entity +@Table(name = "USERS") +public class User extends BaseTimeEntity { + @Id + @Column(name = "USER_ID") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "EMAIL", length = 45, unique = true) + private String email; + + @Column(name = "PASSWORD", length = 100) + private String password; + + @Column(name = "NICKNAME", length = 45) + private String nickname; + + @Column(name = "PHONE_NUMBER", length = 20) + private String phonenumber; + + @Column(name = "ROLE") + @Enumerated(EnumType.STRING) + private Role role; + + @OneToMany(mappedBy = "user") + private List posts = new ArrayList<>(); + +// @ManyToMany(mappedBy = "interest_posts") +// @Column(name = "INTEREST_POST") +// private List interests = new ArrayList<>(); + + @Builder + private User(String email, String nickname, String password, String phone_number) { + this.email = email; + this.password = password; + this.nickname = nickname; + this.phonenumber = phonenumber; + this.role = Role.USER; + } + public void encodePassword(PasswordEncoder passwordEncoder) { + this.password = passwordEncoder.encode(password); + } + + public void addUserAuthority() { + this.role = Role.USER; + } +} diff --git a/src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java b/src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java new file mode 100644 index 0000000..e5fa216 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java @@ -0,0 +1,7 @@ +package com.cha.carrotApi.exception; + +public class CannotConvertHelperException extends RuntimeException{ + public CannotConvertHelperException(String message) { + super(message); + } +} diff --git a/src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java new file mode 100644 index 0000000..bb1c6bd --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class CategoryNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/CustomException.java b/src/main/java/com/cha/carrotApi/exception/CustomException.java new file mode 100644 index 0000000..e37a820 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/CustomException.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class CustomException extends RuntimeException{ + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/cha/carrotApi/exception/ErrorCode.java b/src/main/java/com/cha/carrotApi/exception/ErrorCode.java new file mode 100644 index 0000000..7b93f5a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/ErrorCode.java @@ -0,0 +1,23 @@ +package com.cha.carrotApi.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + USER_EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 이메일을 가진 회원을 찾을 수 없습니다."), + USER_EMAIL_DUPLICATED(HttpStatus.CONFLICT, "이미 가입된 이메일입니다."), + USER_PASSWORD_INVALID(HttpStatus.UNAUTHORIZED, "비밀번호가 틀렸습니다."), + USER_NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "닉네임이 이미 존재합니다."), + USER_PASSWORD_INSERT_ERROR(HttpStatus.UNAUTHORIZED, "비밀번호를 정확하게 입력해주세요"), + USER_PHONE_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "동일한 전화번호가 존재합니다."), + DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "데이터가 이미 존재합니다.") + + ; + + private final HttpStatus httpStatus; + private final String message; + +} diff --git a/src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java b/src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java new file mode 100644 index 0000000..24b2bc8 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java @@ -0,0 +1,8 @@ +package com.cha.carrotApi.exception; + +import java.io.IOException; + +public class FileUploadFailureException extends RuntimeException{ + public FileUploadFailureException(IOException e) { + } +} diff --git a/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java b/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..19e2844 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java @@ -0,0 +1,30 @@ +package com.cha.carrotApi.exception; + +import com.cha.carrotApi.DTO.exception.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import javax.validation.ConstraintViolationException; + +import static com.cha.carrotApi.exception.ErrorCode.DUPLICATE_RESOURCE; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(value = { ConstraintViolationException.class, DataIntegrityViolationException.class}) + protected ResponseEntity handleDataException() { + log.error("handleDataException throw Exception : {}", DUPLICATE_RESOURCE); + return ErrorResponse.toResponseEntity(DUPLICATE_RESOURCE); + } + + @ExceptionHandler(value = { CustomException.class}) + protected ResponseEntity handleCustomException(CustomException e) { + log.error("handleCustomException throw CustomException : {}", e.getErrorCode()); + return ErrorResponse.toResponseEntity(e.getErrorCode()); + } +} \ No newline at end of file diff --git a/src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java new file mode 100644 index 0000000..539db87 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class InterestedNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java new file mode 100644 index 0000000..5d4bbc7 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class PostNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java b/src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java new file mode 100644 index 0000000..80a8de4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class UnsupportedImageFormatException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java b/src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java new file mode 100644 index 0000000..ca24ca3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class UserNotEqualsException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java new file mode 100644 index 0000000..e869e49 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class UserNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java new file mode 100644 index 0000000..66fdb42 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java @@ -0,0 +1,23 @@ +package com.cha.carrotApi.jwt_security; + +import com.cha.carrotApi.repository.User.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Transactional +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return (UserDetails) userRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java new file mode 100644 index 0000000..f0ee565 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java @@ -0,0 +1,48 @@ +package com.cha.carrotApi.jwt_security; + +import com.cha.carrotApi.DTO.exception.SuccessResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RequiredArgsConstructor +@Slf4j +@Component +public class Interceptor implements HandlerInterceptor { + private final ObjectMapper objectMapper; + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object object, + Exception ex + ) throws Exception { + final ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response; + + if (!String.valueOf(response.getStatus()).startsWith("2")) { + return; + } + if (cachingResponse.getContentType() != null && (cachingResponse.getContentType().contains("application/json"))) { + String body = new String(cachingResponse.getContentAsByteArray()); + + Object data = objectMapper.readValue(body, Object.class); + + SuccessResponse objectResponseDto = new SuccessResponse<>(data); + + String wrappedBody = objectMapper.writeValueAsString(objectResponseDto); + + cachingResponse.resetBuffer(); + + cachingResponse.getOutputStream().write(wrappedBody.getBytes(), 0, wrappedBody.getBytes().length); + log.info("Response Body : {}", wrappedBody); + } + } + +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..b16794a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java @@ -0,0 +1,43 @@ +package com.cha.carrotApi.jwt_security; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper((HttpServletRequest) request); + ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper((HttpServletResponse) response); + + String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); + + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(wrappingRequest, wrappingResponse); + wrappingResponse.copyBodyToResponse(); + } +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java new file mode 100644 index 0000000..28a3cbb --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java @@ -0,0 +1,77 @@ +package com.cha.carrotApi.jwt_security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +@RequiredArgsConstructor +@Component +@Slf4j +public class JwtTokenProvider { + @Value("${secret-key}") + private String secretKey; + + // 토큰 유효시간 168 시간(7일) + private long tokenValidTime = 1440 * 60 * 7 * 1000L; + private final CustomUserDetailsService customUserDetailsService; + + // 객체 초기화, secretKey 를 Base64로 인코딩합니다. + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + // JWT 토큰 생성 + public String createToken(String userEmail, List roles) { + Claims claims = Jwts.claims().setSubject(userEmail); // JWT payload 에 저장되는 정보단위 + claims.put("roles", roles); // 정보는 key/value 쌍으로 저장됩니다. + Date now = new Date(); + return Jwts.builder() + .setClaims(claims) // 정보 저장 + .setIssuedAt(now) // 토큰 발행 시간 정보 + .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time + .signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘 + // signature 에 들어갈 secret 값 세팅 + .compact(); + } + + // JWT 토큰에서 인증 정보 조회 + public Authentication getAuthentication(String token) { + UserDetails userDetails = customUserDetailsService.loadUserByUsername(this.getUserEmail(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + // 토큰에서 회원 정보 추출 + public String getUserEmail(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값' + public String resolveToken(HttpServletRequest request) { + return request.getHeader("X-AUTH-TOKEN"); + } + + // 토큰의 유효성 + 만료일자 확인 + public boolean validateToken(String jwtToken) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken); + return !claims.getBody().getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java new file mode 100644 index 0000000..bc3cf9f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.cha.carrotApi.jwt_security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@EnableWebSecurity +@RequiredArgsConstructor +@Configuration +public class SecurityConfig implements WebMvcConfigurer { + private final JwtTokenProvider jwtTokenProvider; + + private final Interceptor interceptor; + + // 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다. + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http.httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다. + .csrf().disable() // csrf 보안 토큰 disable처리. + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다. + .and() + .authorizeHttpRequests() // 요청에 대한 사용권한 체크 + .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능 + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(interceptor) + .addPathPatterns("/**"); + } +} diff --git a/src/main/java/com/cha/carrotApi/repository/Category/CategoryRepository.java b/src/main/java/com/cha/carrotApi/repository/Category/CategoryRepository.java new file mode 100644 index 0000000..d47a22d --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Category/CategoryRepository.java @@ -0,0 +1,13 @@ +package com.cha.carrotApi.repository.Category; + +import com.cha.carrotApi.domain.Category.Category; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface CategoryRepository extends JpaRepository { + + @Query("SELECT c FROM Category c LEFT JOIN c.parent p ORDER BY p.id ASC NULLS FIRST, c.id ASC") + List findAllOrderByParentId(); +} diff --git a/src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java b/src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java new file mode 100644 index 0000000..8b00384 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi.repository.Post; + +import com.cha.carrotApi.domain.Post.InterestedPost; +import com.cha.carrotApi.domain.Post.Post; +import com.cha.carrotApi.domain.User.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface InterestedRepository extends JpaRepository { + Optional findInterestedByPost(Post post); + Optional findByPostAndUser(Post post, User user); + List findAllByUser(User user); +} diff --git a/src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java b/src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java new file mode 100644 index 0000000..b66468e --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java @@ -0,0 +1,12 @@ +package com.cha.carrotApi.repository.Post; + +import com.cha.carrotApi.domain.Post.LikePost; +import com.cha.carrotApi.domain.Post.Post; +import com.cha.carrotApi.domain.User.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LikeRepository extends JpaRepository { + Optional findByPostAndUser(Post post, User user); +} diff --git a/src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java b/src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java new file mode 100644 index 0000000..50cb879 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java @@ -0,0 +1,14 @@ +package com.cha.carrotApi.repository.Post; + +import com.cha.carrotApi.domain.Post.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + Page findByTitle(String keyword, Pageable pageable); + Page findAll(Pageable pageable); + Page findAllByCategoryId(Pageable pageable, int categoryId); + + Page findByTitleContaining(String keyword, Pageable pageable); +} diff --git a/src/main/java/com/cha/carrotApi/repository/User/UserRepository.java b/src/main/java/com/cha/carrotApi/repository/User/UserRepository.java new file mode 100644 index 0000000..cdcd1e3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/User/UserRepository.java @@ -0,0 +1,14 @@ +package com.cha.carrotApi.repository.User; + +import com.cha.carrotApi.domain.User.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByNickname(String nickname); + Optional findByPhonenumber(String phonenumber); +} diff --git a/src/main/java/com/cha/carrotApi/response/Failure.java b/src/main/java/com/cha/carrotApi/response/Failure.java new file mode 100644 index 0000000..466af20 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Failure.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Failure implements Result{ + private String msg; +} diff --git a/src/main/java/com/cha/carrotApi/response/Response.java b/src/main/java/com/cha/carrotApi/response/Response.java new file mode 100644 index 0000000..2d77959 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Response.java @@ -0,0 +1,27 @@ +package com.cha.carrotApi.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Response { + private boolean success; + private int code; + private Result result; + + public static Response success() { + return new Response(true, 0, null); + } + + public static Response success(T data) { + return new Response(true, 0, new Success<>(data)); + } + + public static Response failure(int code, String msg) { + return new Response(false, code, new Failure(msg)); + } +} diff --git a/src/main/java/com/cha/carrotApi/response/Result.java b/src/main/java/com/cha/carrotApi/response/Result.java new file mode 100644 index 0000000..82a639f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Result.java @@ -0,0 +1,3 @@ +package com.cha.carrotApi.response; +interface Result { +} diff --git a/src/main/java/com/cha/carrotApi/response/Success.java b/src/main/java/com/cha/carrotApi/response/Success.java new file mode 100644 index 0000000..99c67ac --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Success.java @@ -0,0 +1,12 @@ +package com.cha.carrotApi.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Success implements Result { + private T data; +} diff --git a/src/main/java/com/cha/carrotApi/service/Category/CategoryService.java b/src/main/java/com/cha/carrotApi/service/Category/CategoryService.java new file mode 100644 index 0000000..7ab78a1 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/Category/CategoryService.java @@ -0,0 +1,46 @@ +package com.cha.carrotApi.service.Category; + +import com.cha.carrotApi.DTO.category.CategoryCreateRequest; +import com.cha.carrotApi.DTO.category.CategoryDto; +import com.cha.carrotApi.domain.Category.Category; +import com.cha.carrotApi.exception.CategoryNotFoundException; +import com.cha.carrotApi.repository.Category.CategoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CategoryService { + private final static String DEFAULT_CATEGORY = "Default"; + private final CategoryRepository categoryRepository; + + @Transactional(readOnly = true) + public List findAllCategory() { + List categories = categoryRepository.findAllOrderByParentId(); + return CategoryDto.toDtoList(categories); + } + + @Transactional + public void createAtFirst() { + Category category = new Category(DEFAULT_CATEGORY, null); + categoryRepository.save(category); + } + + @Transactional + public void createCategory(CategoryCreateRequest req) { + Category parent = Optional.ofNullable(req.getParentId()) + .map(id -> categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new)) + .orElse(null); + categoryRepository.save(new Category(req.getName(), parent)); + } + + @Transactional + public void deleteCategory(int id) { + Category category = categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new); + categoryRepository.delete(category); + } +} diff --git a/src/main/java/com/cha/carrotApi/service/FileService.java b/src/main/java/com/cha/carrotApi/service/FileService.java new file mode 100644 index 0000000..020770a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/FileService.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.service; + +import org.springframework.context.annotation.Primary; +import org.springframework.web.multipart.MultipartFile; + +@Primary +public interface FileService { + void upload(MultipartFile file, String filename); + void delete(String filename); +} diff --git a/src/main/java/com/cha/carrotApi/service/LocalFileService.java b/src/main/java/com/cha/carrotApi/service/LocalFileService.java new file mode 100644 index 0000000..428d154 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/LocalFileService.java @@ -0,0 +1,42 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.exception.FileUploadFailureException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.IOException; + +@Service +@Slf4j +@PropertySource("classpath:application.yml") +public class LocalFileService implements FileService{ + @Value("${itemImgLocation}") + private String location; + + @PostConstruct + void postConstruct() { + File dir = new File(location); + if (!dir.exists()) { + dir.mkdir(); + } + } + + @Override + public void upload(MultipartFile file, String filename) { + try { + file.transferTo(new File(location + filename)); + } catch(IOException e) { + throw new FileUploadFailureException(e); + } + } + + @Override + public void delete(String filename) { + new File(location + filename).delete(); + } +} diff --git a/src/main/java/com/cha/carrotApi/service/Post/PostService.java b/src/main/java/com/cha/carrotApi/service/Post/PostService.java new file mode 100644 index 0000000..e67703f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/Post/PostService.java @@ -0,0 +1,180 @@ +package com.cha.carrotApi.service.Post; + +import com.cha.carrotApi.DTO.post.*; +import com.cha.carrotApi.domain.Category.Category; +import com.cha.carrotApi.domain.Post.Image; +import com.cha.carrotApi.domain.Post.InterestedPost; +import com.cha.carrotApi.domain.Post.LikePost; +import com.cha.carrotApi.domain.Post.Post; +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.exception.CategoryNotFoundException; +import com.cha.carrotApi.exception.InterestedNotFoundException; +import com.cha.carrotApi.exception.PostNotFoundException; +import com.cha.carrotApi.exception.UserNotEqualsException; +import com.cha.carrotApi.repository.Category.CategoryRepository; +import com.cha.carrotApi.repository.Post.InterestedRepository; +import com.cha.carrotApi.repository.Post.LikeRepository; +import com.cha.carrotApi.repository.Post.PostRepository; +import com.cha.carrotApi.service.FileService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.util.stream.Collectors.*; + +@Service +@RequiredArgsConstructor +public class PostService { + private final static String SUCCESS_LIKE_POST = "좋아요 처리"; + private final static String SUCCESS_UNLIKE_POST = "좋아요 취소"; + private final static String SUCCESS_INTEREST_POST = "관심글 처리"; + private final static String SUCCESS_NOT_INTEREST_POST = "관심글 취소"; + + private final PostRepository postRepository; + private final FileService fileService; + private final LikeRepository likeRepository; + private final InterestedRepository interestedRepository; + private final int RECOMMEND_SET_COUNT = 10; + private final CategoryRepository categoryRepository; + + //글 생성 + @Transactional + public PostCreateResponse createPost(PostCreateRequest req, int categoryId, User user) { + List images = req.getImages().stream() + .map(i -> new Image(i.getOriginalFilename())) + .collect(toList()); + Category category = categoryRepository.findById(categoryId).orElseThrow(CategoryNotFoundException::new); + Post post = postRepository.save(new Post(user, req.getTitle(), req.getContent(), category, images)); + uploadImages(post.getImages(), req.getImages()); + return new PostCreateResponse(post.getId(), post.getTitle(), post.getContent()); + } + + //전체 글 조회 + @Transactional(readOnly = true) + public PostFindAll findAllPosts(Integer page, int categoryId) { + Page posts = makePagePosts(page, categoryId); + return responsePagingPosts(posts); + } + + private PostFindAll responsePagingPosts(Page posts) { + List postSimpleDtoList = posts.stream() + .map(i -> new PostSimpleDto().toDto(i)) + .collect(toList()); + return PostFindAll.toDto(postSimpleDtoList, new PageInfoDto(posts)); + } + + private Page makePagePosts(Integer page, int categoryId) { + PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("id").descending()); + Page posts = postRepository.findAllByCategoryId(pageRequest, categoryId); + return posts; + } + + private void uploadImages(List images, List fileImages) { + IntStream.range(0, images.size()) + .forEach(i -> fileService.upload(fileImages.get(i), images.get(i).getUniqueName())); + } + + private void deleteImages(List images) { + images.forEach(i -> fileService.delete(i.getUniqueName())); + } + + @Transactional(readOnly = true) + public PostResponseDto findPost(Long id) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + User user = post.getUser(); + return PostResponseDto.toDto(post, user.getNickname()); + } + + @Transactional + public String updateLikeOfPost(Long id, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + if (!likeRepository.findByPostAndUser(post, user).isPresent()) { + post.increaseLikeCount(); + return createLikePost(post, user); + } + post.decreaseLikeCount(); + return removeLikePost(post, user); + } + @Transactional + public String updateInterestedPost(Long id, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + if (!interestedRepository.findByPostAndUser(post, user).isPresent()) { + post.increaseInterested(); + return createInterestedPost(post, user); + } + post.decreaseInterested(); + return removeInterestedPost(post, user); + } + + @Transactional + public PostResponseDto editPost(Long id, PostUpdateRequest req, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + validatePostOwner(user, post); + Post.ImageUpdatedResult result = post.update(req); + uploadImages(result.getAddedImages(), result.getAddedImageFiles()); + deleteImages(result.getDeletedImages()); + return PostResponseDto.toDto(post, user.getNickname()); + } + + @Transactional + public void deletePost(Long id, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + validatePostOwner(user, post); + postRepository.delete(post); + } + + @Transactional + public List searchPost(String keyword, Pageable pageable) { + Page posts = postRepository.findByTitleContaining(keyword, pageable); + List postSimpleDtoList = posts.stream() + .map(i -> new PostSimpleDto().toDto(i)) + .collect(toList()); + return postSimpleDtoList; + } + + + private void validatePostOwner(User user, Post post) { + if (!user.equals(post.getUser())) { + throw new UserNotEqualsException(); + } + } + + public String createLikePost(Post board, User user) { + LikePost likePost = new LikePost(board, user); // true 처리 + likeRepository.save(likePost); + return SUCCESS_LIKE_POST; + } + + private String removeLikePost(Post post, User user) { + LikePost likePost = likeRepository.findByPostAndUser(post, user).orElseThrow(() -> { + throw new IllegalArgumentException("'좋아요' 기록을 찾을 수 없습니다."); + }); + likeRepository.delete(likePost); + return SUCCESS_UNLIKE_POST; + } + + private String createInterestedPost(Post post, User user) { + InterestedPost interestedPost = new InterestedPost(post, user); + interestedRepository.save(interestedPost); + return SUCCESS_INTEREST_POST; + } + + private String removeInterestedPost(Post post, User user) { + InterestedPost interestedPost = interestedRepository.findByPostAndUser(post, user) + .orElseThrow(InterestedNotFoundException::new); + interestedRepository.delete(interestedPost); + return SUCCESS_NOT_INTEREST_POST; + } + + + +} diff --git a/src/main/java/com/cha/carrotApi/service/User/UserService.java b/src/main/java/com/cha/carrotApi/service/User/UserService.java new file mode 100644 index 0000000..80fabb8 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/User/UserService.java @@ -0,0 +1,66 @@ +package com.cha.carrotApi.service.User; + +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.exception.CustomException; +import com.cha.carrotApi.exception.ErrorCode; +import com.cha.carrotApi.jwt_security.JwtTokenProvider; +import com.cha.carrotApi.repository.User.UserRepository; +import com.cha.carrotApi.DTO.user.SignUpRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public Long signUp(SignUpRequest requestDto) throws Exception { + + checkUser(requestDto); + + User user = userRepository.save(requestDto.toEntity()); + user.encodePassword(passwordEncoder); + + user.addUserAuthority(); + return user.getId(); + } + + @Transactional + public String login(Map users) { + User user = userRepository.findByEmail(users.get("email")) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + String password = users.get("password"); + if (!passwordEncoder.matches(password,user.getPassword())) { + throw new CustomException(ErrorCode.USER_PASSWORD_INVALID); + } + + List roles = new ArrayList<>(); + roles.add(user.getRole().name()); + + return jwtTokenProvider.createToken(user.getEmail(), roles); + } + + public boolean checkUser (SignUpRequest requestDto) { + if (userRepository.findByEmail(requestDto.getEmail()).isPresent()) { + throw new CustomException(ErrorCode.USER_EMAIL_DUPLICATED); + } + if (userRepository.findByNickname(requestDto.getNickname()).isPresent()){ + throw new CustomException(ErrorCode.USER_NICKNAME_DUPLICATED); + } + if (userRepository.findByPhonenumber(requestDto.getPhonenumber()).isPresent()) { + throw new CustomException(ErrorCode.USER_PHONE_NUMBER_DUPLICATED); + } + return true; + } +} diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java deleted file mode 100644 index ef164c9..0000000 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dku.springstudy; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SpringStudyApplication { - - public static void main(String[] args) { - SpringApplication.run(SpringStudyApplication.class, args); - } - -} diff --git a/src/main/resources/application-API-KEY.properties b/src/main/resources/application-API-KEY.properties new file mode 100644 index 0000000..e57b090 --- /dev/null +++ b/src/main/resources/application-API-KEY.properties @@ -0,0 +1 @@ +secret-key = c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e26d141 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,39 @@ + +spring: + datasource: + url: jdbc:h2:tcp://localhost/~/carrotapi + username: sa + password: + driver-class-name: org.h2.Driver + initialization-mode: always + profiles: + include: API-KEY + security: + jwt: + header: Authorization + secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK + token-validity-in-seconds: 86400 + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + # show_sql: true + format_sql: true + defer-datasource-initialization: true + sql: + init: + mode: always + schema-locations: classpath:schema.sql + +logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace + +server: + error: + include-exception: false + include-message: always + include-stacktrace: on_param + whitelabel.enabled: true +itemImgLocation: C:/Users/cha_hammin/Desktop/image \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..8efff75 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,18 @@ +insert into category(category_id, name, parent_id) values(1,'DEFAULT_CATEGORY', null); +insert into category(category_id, name, parent_id) values(2,'디지털기기',1); +insert into category(category_id, name, parent_id) values(3,'생활가전',1); +insert into category(category_id, name, parent_id) values(4,'가구/인테리어',1); +insert into category(category_id, name, parent_id) values(5,'유아동',1); +insert into category(category_id, name, parent_id) values(6,'생활/가공식품',1); +insert into category(category_id, name, parent_id) values(7,'유아도서',1); +insert into category(category_id, name, parent_id) values(8,'스포츠/레저',1); +insert into category(category_id, name, parent_id) values(9,'여성잡화',1); +insert into category(category_id, name, parent_id) values(10,'여성의류',1); +insert into category(category_id, name, parent_id) values(11,'남성패션/잡화',1); +insert into category(category_id, name, parent_id) values(12,'게임/취미',1); +insert into category(category_id, name, parent_id) values(13,'뷰티/미용',1); +insert into category(category_id, name, parent_id) values(14,'반려동물용품',1); +insert into category(category_id, name, parent_id) values(15,'도서/티켓/음반',1); +insert into category(category_id, name, parent_id) values(16,'식물',1); +insert into category(category_id, name, parent_id) values(17,'기타 중고물품',1); +insert into category(category_id, name, parent_id) values(18,'중고차',1); \ No newline at end of file diff --git a/src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java b/src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java similarity index 69% rename from src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java rename to src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java index 79d9975..c6e2a31 100644 --- a/src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java +++ b/src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java @@ -1,10 +1,10 @@ -package com.dku.springstudy; +package com.cha.carrotApi; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class SpringStudyApplicationTests { +class CarrotApiApplicationTests { @Test void contextLoads() {