diff --git a/build.gradle b/build.gradle index 1e28f16..305ea04 100644 --- a/build.gradle +++ b/build.gradle @@ -1,36 +1,50 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.0.11' - id 'io.spring.dependency-management' version '1.1.3' + id 'java' + id 'org.springframework.boot' version '3.2.0' + id 'io.spring.dependency-management' version '1.1.4' } -group = 'com.server.sopt' +group = 'com.sopt.server' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + //mysql + implementation 'mysql:mysql-connector-java:8.0.23' + // AWS sdk + implementation("software.amazon.awssdk:bom:2.21.0") + implementation("software.amazon.awssdk:s3:2.21.0") + + // security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' } + tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c..7f93135 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f4197d..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index fcb6fca..1aa94a4 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/settings.gradle b/settings.gradle index 6b30e33..ff2a15a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'seminar' +rootProject.name = 'seminar_week6' diff --git a/src/main/java/com/server/sopt/seminar/ServerSeminarApplication.java b/src/main/java/com/server/sopt/seminar/ServerSeminarApplication.java deleted file mode 100644 index 026af16..0000000 --- a/src/main/java/com/server/sopt/seminar/ServerSeminarApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.server.sopt.seminar; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ServerSeminarApplication { - - public static void main(String[] args) { - SpringApplication.run(ServerSeminarApplication.class, args); - } - -} diff --git a/src/main/java/com/server/sopt/seminar/controller/HealthCheckController.java b/src/main/java/com/server/sopt/seminar/controller/HealthCheckController.java deleted file mode 100644 index 991bfce..0000000 --- a/src/main/java/com/server/sopt/seminar/controller/HealthCheckController.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.server.sopt.seminar.controller; - -import com.server.sopt.seminar.dto.HealthCheckResponse; -import com.server.sopt.seminar.dto.Response; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.HashMap; -import java.util.Map; - -@RestController -@RequestMapping("/health") -public class HealthCheckController { - - @GetMapping("/v1" ) - public Map healthCheck() { - Map response = new HashMap<>(); - response.put("status", "OK"); - return response; - } - - @GetMapping("/v2") - public ResponseEntity healthCheckV2() { - return ResponseEntity.ok("OK"); - } - - @GetMapping("v3") - public String healthCheck3(){ - return ("OK"); - } - @GetMapping("/v4") - public ResponseEntity> healthCheckV4() { - Map response = new HashMap<>(); - response.put("status", "OK"); - return ResponseEntity.ok(response); - } - - @GetMapping("/v5") - public ResponseEntity healthCheckV5() { - return ResponseEntity.ok(new HealthCheckResponse()); - } - - @GetMapping("/v6") - public Response healthCheckV6() { - return Response.of(200, "OK", true); - } - - - - -} \ No newline at end of file diff --git a/src/main/java/com/server/sopt/seminar/dto/HealthCheckResponse.java b/src/main/java/com/server/sopt/seminar/dto/HealthCheckResponse.java deleted file mode 100644 index b2f2273..0000000 --- a/src/main/java/com/server/sopt/seminar/dto/HealthCheckResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.server.sopt.seminar.dto; - -public class HealthCheckResponse { - private static final String STATUS = "OK"; - private String status; - public HealthCheckResponse(String status) { - this.status = status; - } - - public HealthCheckResponse(){ - this.status = STATUS; - } -} - - - diff --git a/src/main/java/com/server/sopt/seminar/dto/Response.java b/src/main/java/com/server/sopt/seminar/dto/Response.java deleted file mode 100644 index 6f8af95..0000000 --- a/src/main/java/com/server/sopt/seminar/dto/Response.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.server.sopt.seminar.dto; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Builder(access = AccessLevel.PRIVATE) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@Getter -public class Response { - private int code; - private String status; - private boolean success; - - public static Response of(int code, String status, boolean success){ - return Response.builder() - .code(code) - .status(status) - .success(success) - .build(); - } -} diff --git a/src/main/java/com/sopt/server/seminar_week6/SeminarWeek6Application.java b/src/main/java/com/sopt/server/seminar_week6/SeminarWeek6Application.java new file mode 100644 index 0000000..b894115 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/SeminarWeek6Application.java @@ -0,0 +1,13 @@ +package com.sopt.server.seminar_week6; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SeminarWeek6Application { + + public static void main(String[] args) { + SpringApplication.run(SeminarWeek6Application.class, args); + } + +} diff --git a/src/main/java/com/sopt/server/seminar_week6/config/AWSConfig.java b/src/main/java/com/sopt/server/seminar_week6/config/AWSConfig.java new file mode 100644 index 0000000..41be52b --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/config/AWSConfig.java @@ -0,0 +1,48 @@ +package com.sopt.server.seminar_week6.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class AWSConfig { + + private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId"; + private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + + private final String accessKey; + private final String secretKey; + private final String regionString; + + public AWSConfig(@Value("${aws-property.access-key}") final String accessKey, + @Value("${aws-property.secret-key}") final String secretKey, + @Value("${aws-property.aws-region}") final String regionString) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.regionString = regionString; + } + + + @Bean + public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() { + System.setProperty(AWS_ACCESS_KEY_ID, accessKey); + System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey); + return SystemPropertyCredentialsProvider.create(); + } + + @Bean + public Region getRegion() { + return Region.of(regionString); + } + + @Bean + public S3Client getS3Client() { + return S3Client.builder() + .region(getRegion()) + .credentialsProvider(systemPropertyCredentialsProvider()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/config/BCryptPasswordConfig.java b/src/main/java/com/sopt/server/seminar_week6/config/BCryptPasswordConfig.java new file mode 100644 index 0000000..1bd4e4f --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/config/BCryptPasswordConfig.java @@ -0,0 +1,17 @@ +package com.sopt.server.seminar_week6.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 BCryptPasswordConfig { + + private static final int STRENGTH = 10; + + @Bean + public PasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(STRENGTH); + } +} diff --git a/src/main/java/com/sopt/server/seminar_week6/config/SecurityConfig.java b/src/main/java/com/sopt/server/seminar_week6/config/SecurityConfig.java new file mode 100644 index 0000000..bd5ae15 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.sopt.server.seminar_week6.config; + +import com.sopt.server.seminar_week6.external.CustomAccessDeniedHandler; +import com.sopt.server.seminar_week6.external.CustomJwtAuthenticationEntryPoint; +import com.sopt.server.seminar_week6.external.JwtAuthenticationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; + +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; + +@Configuration +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + private static final String[] AUTH_WHITELIST = {"/api/users/sign-up", "/api/users/sign-in"}; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, + CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint, + CustomAccessDeniedHandler customAccessDeniedHandler) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.customJwtAuthenticationEntryPoint = customJwtAuthenticationEntryPoint; + this.customAccessDeniedHandler = customAccessDeniedHandler; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf().disable() + .formLogin().disable() + .httpBasic().disable() + .sessionManagement() + .sessionCreationPolicy(STATELESS) + .and() + .exceptionHandling() + .authenticationEntryPoint(customJwtAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + .and() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .addFilterBefore(jwtAuthenticationFilter, AbstractPreAuthenticatedProcessingFilter.class) + .build(); + } + + +} diff --git a/src/main/java/com/sopt/server/seminar_week6/controller/PostController.java b/src/main/java/com/sopt/server/seminar_week6/controller/PostController.java new file mode 100644 index 0000000..3e6f913 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/controller/PostController.java @@ -0,0 +1,53 @@ +package com.sopt.server.seminar_week6.controller; + +import com.sopt.server.seminar_week6.dto.PostCreateRequest; +import com.sopt.server.seminar_week6.dto.PostGetResponse; +import com.sopt.server.seminar_week6.dto.PostUpdateRequest; +import com.sopt.server.seminar_week6.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +public class PostController { + private static final String CUSTOM_AUTH_ID = "X-Auth-Id"; + + private final PostService postService; + + @GetMapping("{postId}") + public ResponseEntity getPostById(@PathVariable Long postId) { + return ResponseEntity.ok(postService.getById(postId)); + } + + @GetMapping + public ResponseEntity> getPosts(@RequestHeader(CUSTOM_AUTH_ID) Long memberId) { + return ResponseEntity.ok(postService.getPosts(memberId)); + } + + @PostMapping + public ResponseEntity createPost(@RequestHeader(CUSTOM_AUTH_ID) Long memberId, + @RequestBody PostCreateRequest request) { + URI location = URI.create("/api/post/" + postService.create(request, memberId)); + return ResponseEntity.created(location).build(); + } + + @PatchMapping("{postId}") + public ResponseEntity updatePost(@PathVariable Long postId, @RequestBody PostUpdateRequest request) { + postService.editContent(postId, request); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("{postId}") + public ResponseEntity deletePost(@PathVariable Long postId) { + postService.deleteById(postId); + return ResponseEntity.noContent().build(); + } +} + + + diff --git a/src/main/java/com/sopt/server/seminar_week6/controller/PostControllerV2.java b/src/main/java/com/sopt/server/seminar_week6/controller/PostControllerV2.java new file mode 100644 index 0000000..536263e --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/controller/PostControllerV2.java @@ -0,0 +1,34 @@ +package com.sopt.server.seminar_week6.controller; + +import com.sopt.server.seminar_week6.dto.PostCreateRequest; +import com.sopt.server.seminar_week6.service.PostServiceV2; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URI; +import java.security.Principal; + +@RestController +@RequestMapping("/api/v2/posts") +@RequiredArgsConstructor +public class PostControllerV2 { + + private static final String CUSTOM_AUTH_ID = "X-Auth-Id"; + private final PostServiceV2 postService; + + @PostMapping + public ResponseEntity createPostV2( + @RequestHeader(CUSTOM_AUTH_ID) Long memberId, @RequestPart MultipartFile image, PostCreateRequest request) { + URI location = URI.create("/api/posts/v2" + postService.createV2(request, image, memberId)); + return ResponseEntity.created(location).build(); + } + + @DeleteMapping("/{postId}") + public ResponseEntity deletePost(@PathVariable Long postId) { + postService.deleteByIdV2(postId); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/com/sopt/server/seminar_week6/controller/ServiceMemberController.java b/src/main/java/com/sopt/server/seminar_week6/controller/ServiceMemberController.java new file mode 100644 index 0000000..78f9a91 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/controller/ServiceMemberController.java @@ -0,0 +1,39 @@ +package com.sopt.server.seminar_week6.controller; + +import com.sopt.server.seminar_week6.domain.ServiceMember; +import com.sopt.server.seminar_week6.dto.PostCreateRequest; +import com.sopt.server.seminar_week6.dto.ServiceMemberRequest; +import com.sopt.server.seminar_week6.service.ServiceMemberService; +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 java.net.URI; +import java.security.Principal; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/users") +public class ServiceMemberController { + + private final ServiceMemberService serviceMemberService; + + + @PostMapping("sign-up") + public ResponseEntity signUp(@RequestBody ServiceMemberRequest request){ + URI location = URI.create(serviceMemberService.create(request)); + return ResponseEntity.created(location).build(); + } + + @PostMapping("sign-in") + public ResponseEntity signIn(@RequestBody ServiceMemberRequest request){ + serviceMemberService.signIn(request); + return ResponseEntity.noContent().build(); + } + + + +} diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/BaseTimeEntity.java b/src/main/java/com/sopt/server/seminar_week6/domain/BaseTimeEntity.java new file mode 100644 index 0000000..30f5529 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/BaseTimeEntity.java @@ -0,0 +1,21 @@ +package com.sopt.server.seminar_week6.domain; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +//BaseEntity 데이터베이스 테이블에 공통적으로 들어가는 컬럼을 모아둔 클래스 +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/CategoryId.java b/src/main/java/com/sopt/server/seminar_week6/domain/CategoryId.java new file mode 100644 index 0000000..322e795 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/CategoryId.java @@ -0,0 +1,5 @@ +package com.sopt.server.seminar_week6.domain; + +public enum CategoryId { + +} diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/JwtValidationType.java b/src/main/java/com/sopt/server/seminar_week6/domain/JwtValidationType.java new file mode 100644 index 0000000..06f42b7 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/JwtValidationType.java @@ -0,0 +1,10 @@ +package com.sopt.server.seminar_week6.domain; + +public enum JwtValidationType { + VALID_JWT, // 유효한 JWT + INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 + INVALID_JWT_TOKEN, // 유효하지 않은 토큰 + EXPIRED_JWT_TOKEN, // 만료된 토큰 + UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 + EMPTY_JWT // 빈 JWT +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/Member.java b/src/main/java/com/sopt/server/seminar_week6/domain/Member.java new file mode 100644 index 0000000..d93a62f --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/Member.java @@ -0,0 +1,38 @@ +package com.sopt.server.seminar_week6.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String nickname; + private int age; + + @Embedded + private Sopt sopt; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) //CascadeType.ALL: member가 삭제되면 post도 삭제됨 + private List posts = new ArrayList<>(); + + @Builder + public Member(String name, String nickname, int age, Sopt sopt) { + this.name = name; + this.nickname = nickname; + this.age = age; + this.sopt = sopt; + } + + public void updateSOPT(Sopt sopt) {} +} diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/Part.java b/src/main/java/com/sopt/server/seminar_week6/domain/Part.java new file mode 100644 index 0000000..5b17e16 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/Part.java @@ -0,0 +1,17 @@ +package com.sopt.server.seminar_week6.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Part { + SERVER("서버"), + WEB("웹"), + ANDROID("안드로이드"), + IOS("iOS"), + PLAN("기획"), + DESIGN("디자인"); + + private final String name; +} diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/Post.java b/src/main/java/com/sopt/server/seminar_week6/domain/Post.java new file mode 100644 index 0000000..a5d5305 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/Post.java @@ -0,0 +1,56 @@ +package com.sopt.server.seminar_week6.domain; + +import com.sopt.server.seminar_week6.domain.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Locale; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "post") //post Table이랑 매핑 +public class Post extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Column(columnDefinition = "TEXT") // varchar대신 Text. 길이 제한 없는 텍스트 타입 + private String content; + + private String imageUrl; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "member_id") // 일대다 관계에서 다 부분이 외래키를 갖음(연관관계의 주인) + private Member member; + + @Column(name = "category_id") + private CategoryId categoryId; + @Builder + public Post(String title, String content, Member member) { + this.title = title; + this.content = content; + this.member = member; + } + + @Builder(builderMethodName = "builderWithImageUrl") + public Post(String title, String content, String imageUrl,Member member) { + this.title = title; + this.content = content; + this.imageUrl = imageUrl; + this.member = member; + } + + public void updateContent(String content) { + this.content = content; + } + public void addCategory(CategoryId categoryId) { + this.categoryId = categoryId; + } +} + diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/ServiceMember.java b/src/main/java/com/sopt/server/seminar_week6/domain/ServiceMember.java new file mode 100644 index 0000000..a876587 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/ServiceMember.java @@ -0,0 +1,28 @@ +package com.sopt.server.seminar_week6.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ServiceMember { + + @Id + @GeneratedValue + private Long id; + + private String nickname; + private String password; + + @Builder + public ServiceMember(String nickname, String password) { + this.nickname = nickname; + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/domain/Sopt.java b/src/main/java/com/sopt/server/seminar_week6/domain/Sopt.java new file mode 100644 index 0000000..468491b --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/domain/Sopt.java @@ -0,0 +1,20 @@ +package com.sopt.server.seminar_week6.domain; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.EnumType.STRING; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Sopt { + private int generation; + @Enumerated(value = STRING) + private Part part; +} diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/MemberCreateRequest.java b/src/main/java/com/sopt/server/seminar_week6/dto/MemberCreateRequest.java new file mode 100644 index 0000000..edd2466 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/MemberCreateRequest.java @@ -0,0 +1,12 @@ +package com.sopt.server.seminar_week6.dto; + +import com.sopt.server.seminar_week6.domain.Sopt; +import lombok.Data; + +@Data +public class MemberCreateRequest { + private String name; + private String nickname; + private int age; + private Sopt sopt; +} diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/MemberGetResponse.java b/src/main/java/com/sopt/server/seminar_week6/dto/MemberGetResponse.java new file mode 100644 index 0000000..3b16c18 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/MemberGetResponse.java @@ -0,0 +1,21 @@ +package com.sopt.server.seminar_week6.dto; + + +import com.sopt.server.seminar_week6.domain.Member; +import com.sopt.server.seminar_week6.domain.Sopt; + +public record MemberGetResponse( + String name, + String nickname, + int age, + Sopt soptInfo +) { + public static MemberGetResponse of(Member member){ + return new MemberGetResponse( + member.getName(), + member.getNickname(), + member.getAge(), + member.getSopt() + ); + } +} diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/MemberProfileUpdateRequest.java b/src/main/java/com/sopt/server/seminar_week6/dto/MemberProfileUpdateRequest.java new file mode 100644 index 0000000..8543943 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/MemberProfileUpdateRequest.java @@ -0,0 +1,10 @@ +package com.sopt.server.seminar_week6.dto; + +import com.sopt.server.seminar_week6.domain.Part; +import lombok.Data; + +@Data +public class MemberProfileUpdateRequest { + private int generation; + private Part part; +} diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/PostCreateRequest.java b/src/main/java/com/sopt/server/seminar_week6/dto/PostCreateRequest.java new file mode 100644 index 0000000..c44f71c --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/PostCreateRequest.java @@ -0,0 +1,6 @@ +package com.sopt.server.seminar_week6.dto; + +public record PostCreateRequest(String title, String content +) +{ +} diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/PostGetResponse.java b/src/main/java/com/sopt/server/seminar_week6/dto/PostGetResponse.java new file mode 100644 index 0000000..9eacfe6 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/PostGetResponse.java @@ -0,0 +1,22 @@ +package com.sopt.server.seminar_week6.dto; + + +import com.sopt.server.seminar_week6.domain.Post; + +public record PostGetResponse( + Long id, + String title, + String content + +) { + public static PostGetResponse of(Post post){ + return new PostGetResponse( + post.getId(), + post.getTitle(), + post.getContent() + + ); + } +} + + diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/PostUpdateRequest.java b/src/main/java/com/sopt/server/seminar_week6/dto/PostUpdateRequest.java new file mode 100644 index 0000000..108b778 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/PostUpdateRequest.java @@ -0,0 +1,4 @@ +package com.sopt.server.seminar_week6.dto; + +public record PostUpdateRequest (String content){ +} diff --git a/src/main/java/com/sopt/server/seminar_week6/dto/ServiceMemberRequest.java b/src/main/java/com/sopt/server/seminar_week6/dto/ServiceMemberRequest.java new file mode 100644 index 0000000..f3356d8 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/dto/ServiceMemberRequest.java @@ -0,0 +1,7 @@ +package com.sopt.server.seminar_week6.dto; + +public record ServiceMemberRequest( + String nickname, + String password +) { +} diff --git a/src/main/java/com/sopt/server/seminar_week6/external/CustomAccessDeniedHandler.java b/src/main/java/com/sopt/server/seminar_week6/external/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..19ea84a --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/external/CustomAccessDeniedHandler.java @@ -0,0 +1,22 @@ +package com.sopt.server.seminar_week6.external; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/external/CustomJwtAuthenticationEntryPoint.java b/src/main/java/com/sopt/server/seminar_week6/external/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..735ea27 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/external/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,20 @@ +package com.sopt.server.seminar_week6.external; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/external/JwtAuthenticationFilter.java b/src/main/java/com/sopt/server/seminar_week6/external/JwtAuthenticationFilter.java new file mode 100644 index 0000000..461a573 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/external/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package com.sopt.server.seminar_week6.external; + + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.sopt.server.seminar_week6.domain.JwtValidationType.VALID_JWT; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + final String token = getJwtFromRequest(request); + if (jwtTokenProvider.validateToken(token) == VALID_JWT) { + Long memberId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = new UserAuthentication(memberId.toString(), null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception exception) { + try { + throw new Exception(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + filterChain.doFilter(request, response); + } + + //HttpServletRequest에서 토큰을 받아오는 메소드 + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring("Bearer ".length()); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/external/JwtTokenProvider.java b/src/main/java/com/sopt/server/seminar_week6/external/JwtTokenProvider.java new file mode 100644 index 0000000..a9d05a1 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/external/JwtTokenProvider.java @@ -0,0 +1,78 @@ +package com.sopt.server.seminar_week6.external; + +import com.sopt.server.seminar_week6.domain.JwtValidationType; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private static final String MEMBER_ID = "memberId"; + + @Value("${jwt.secret}") + private String JWT_SECRET; + + @PostConstruct + protected void init() { + JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(Authentication authentication, Long tokenExpirationTime) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); + + claims.put(MEMBER_ID, authentication.getPrincipal()); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + public JwtValidationType validateToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(MEMBER_ID).toString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/external/S3Service.java b/src/main/java/com/sopt/server/seminar_week6/external/S3Service.java new file mode 100644 index 0000000..59f16a9 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/external/S3Service.java @@ -0,0 +1,77 @@ +package com.sopt.server.seminar_week6.external; + +import com.sopt.server.seminar_week6.config.AWSConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Component +public class S3Service { + + private final String bucketName; + private final AWSConfig awsConfig; + + public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AWSConfig awsConfig) { + this.bucketName = bucketName; + this.awsConfig = awsConfig; + } + + + public String uploadImage(String directoryPath, MultipartFile image) throws IOException { + final String key = directoryPath + generateImageFileName(); + final S3Client s3Client = awsConfig.getS3Client(); + validateExtention(image); + validateFileSize(image); + + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(image.getContentType()) + .contentDisposition("inline") + .build(); + + + RequestBody requestBody = RequestBody.fromBytes(image.getBytes()); + s3Client.putObject(request, requestBody); + return key; + } + + public void deleteImage(String key) throws IOException { + final S3Client s3Client = awsConfig.getS3Client(); + + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucketName) + .key(key) + .build() + ); + } + + private String generateImageFileName() { + return UUID.randomUUID().toString() + ".jpg"; + } + + private static final List IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp"); + private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L; + + + private void validateExtention(MultipartFile image) { + if (!IMAGE_EXTENSIONS.contains(image.getContentType())) { + throw new RuntimeException("이미지 파일만 업로드 가능합니다."); + } + } + + private void validateFileSize(MultipartFile image) { + if (image.getSize() > MAX_FILE_SIZE) { + throw new RuntimeException("이미지 파일은 5MB 이하만 업로드 가능합니다."); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/external/UserAuthentication.java b/src/main/java/com/sopt/server/seminar_week6/external/UserAuthentication.java new file mode 100644 index 0000000..7d67aaf --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/external/UserAuthentication.java @@ -0,0 +1,13 @@ +package com.sopt.server.seminar_week6.external; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/repository/MemberRepository.java b/src/main/java/com/sopt/server/seminar_week6/repository/MemberRepository.java new file mode 100644 index 0000000..ce19d1e --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/repository/MemberRepository.java @@ -0,0 +1,15 @@ +package com.sopt.server.seminar_week6.repository; + + +import com.sopt.server.seminar_week6.domain.Member; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository{ + default Member findByIdOrThrow(Long id){ + return findById(id).orElseThrow(() + -> new EntityNotFoundException("해당하는 id의 회원이 없습니다.")); + } +} diff --git a/src/main/java/com/sopt/server/seminar_week6/repository/PostRepository.java b/src/main/java/com/sopt/server/seminar_week6/repository/PostRepository.java new file mode 100644 index 0000000..bd8691e --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/repository/PostRepository.java @@ -0,0 +1,21 @@ +package com.sopt.server.seminar_week6.repository; + +import com.sopt.server.seminar_week6.domain.Post; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +@Repository +public interface PostRepository extends JpaRepository { + + default Post findByIdOrThrow(Long id){ + return findById(id).orElseThrow(() + -> new EntityNotFoundException("해당하는 id의 게시글이 없습니다.")); + } + List findAllByMemberId(Long memberId); + +} diff --git a/src/main/java/com/sopt/server/seminar_week6/repository/ServiceMemberRepository.java b/src/main/java/com/sopt/server/seminar_week6/repository/ServiceMemberRepository.java new file mode 100644 index 0000000..d96ff6c --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/repository/ServiceMemberRepository.java @@ -0,0 +1,12 @@ +package com.sopt.server.seminar_week6.repository; + +import com.sopt.server.seminar_week6.domain.ServiceMember; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ServiceMemberRepository extends JpaRepository { + Optional findByNickname(String nickname); +} diff --git a/src/main/java/com/sopt/server/seminar_week6/service/PostService.java b/src/main/java/com/sopt/server/seminar_week6/service/PostService.java new file mode 100644 index 0000000..772df0e --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/service/PostService.java @@ -0,0 +1,59 @@ +package com.sopt.server.seminar_week6.service; + +import com.sopt.server.seminar_week6.domain.Member; +import com.sopt.server.seminar_week6.domain.Post; +import com.sopt.server.seminar_week6.dto.PostCreateRequest; +import com.sopt.server.seminar_week6.dto.PostGetResponse; +import com.sopt.server.seminar_week6.dto.PostUpdateRequest; +import com.sopt.server.seminar_week6.repository.MemberRepository; +import com.sopt.server.seminar_week6.repository.PostRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostService { + + private final PostRepository postJpaRepository; + private final MemberRepository memberJpaRepository; + + @Transactional + public String create(PostCreateRequest request, Long memberId) { + Member member = memberJpaRepository.findByIdOrThrow(memberId); + Post post = postJpaRepository.save( + Post.builder() + .member(member) + .title(request.title()) + .content(request.content()).build()); + return post.getId().toString(); + } + @Transactional + public void editContent(Long postId, PostUpdateRequest request) { + Post post = postJpaRepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("해당하는 게시글이 없습니다.")); + post.updateContent(request.content()); + } + + @Transactional + public void deleteById(Long postId) { + Post post = postJpaRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException("해당하는 게시글이 없습니다.")); + postJpaRepository.delete(post); + } + + public List getPosts(Long memberId) { + return postJpaRepository.findAllByMemberId(memberId) + .stream() + .map(post -> PostGetResponse.of(post)) + .toList(); + } + + public PostGetResponse getById(Long postId) { + Post post = postJpaRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException("해당하는 게시글이 없습니다.")); + return PostGetResponse.of(post); + } +} diff --git a/src/main/java/com/sopt/server/seminar_week6/service/PostServiceV2.java b/src/main/java/com/sopt/server/seminar_week6/service/PostServiceV2.java new file mode 100644 index 0000000..03d4e8d --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/service/PostServiceV2.java @@ -0,0 +1,58 @@ +package com.sopt.server.seminar_week6.service; + +import com.sopt.server.seminar_week6.domain.Member; +import com.sopt.server.seminar_week6.domain.Post; +import com.sopt.server.seminar_week6.dto.PostCreateRequest; +import com.sopt.server.seminar_week6.external.S3Service; +import com.sopt.server.seminar_week6.repository.MemberRepository; +import com.sopt.server.seminar_week6.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostServiceV2 { + + private static final String POST_IMAGE_FOLDER_NAME = "posts/"; + + private final MemberRepository memberJpaRepository; + private final PostRepository postJpaRepository; + private final S3Service s3Service; + + + @Transactional + public String createV2(PostCreateRequest request, MultipartFile image, Long memberId) { + try { + final String imageUrl = s3Service.uploadImage(POST_IMAGE_FOLDER_NAME, image); + Member member = memberJpaRepository.findByIdOrThrow(memberId); + Post post = postJpaRepository.save( + Post.builderWithImageUrl() + .title(request.title()) + .content(request.content()) + .imageUrl(imageUrl) + .member(member) + .build()); + return post.getId().toString(); + } catch (RuntimeException | IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Transactional + public void deleteByIdV2(Long postId) { + try { + Post post = postJpaRepository.findById(postId) + .orElseThrow(() -> new RuntimeException("해당하는 id의 게시글이 없습니다.")); + s3Service.deleteImage(post.getImageUrl()); + postJpaRepository.deleteById(postId); + } catch (IOException | RuntimeException e) { + throw new RuntimeException(e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/sopt/server/seminar_week6/service/ServiceMemberService.java b/src/main/java/com/sopt/server/seminar_week6/service/ServiceMemberService.java new file mode 100644 index 0000000..4bcab53 --- /dev/null +++ b/src/main/java/com/sopt/server/seminar_week6/service/ServiceMemberService.java @@ -0,0 +1,35 @@ +package com.sopt.server.seminar_week6.service; + +import com.sopt.server.seminar_week6.domain.ServiceMember; +import com.sopt.server.seminar_week6.dto.ServiceMemberRequest; +import com.sopt.server.seminar_week6.repository.ServiceMemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ServiceMemberService { + private final ServiceMemberRepository servieMemberRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public String create(ServiceMemberRequest request){ + ServiceMember serviceMember = ServiceMember.builder() + .nickname(request.nickname()) + .password(passwordEncoder.encode(request.password())) + .build(); + servieMemberRepository.save(serviceMember); + return serviceMember.getId().toString(); + } + + public void signIn(ServiceMemberRequest request){ + ServiceMember serviceMember = servieMemberRepository.findByNickname(request.nickname()) + .orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다.")); + if(!passwordEncoder.matches(request.password(), serviceMember.getPassword())){ + throw new RuntimeException("비밀번호가 일치하지 않습니다."); + } + } +} diff --git a/src/test/java/com/sopt/server/seminar_week6/SeminarWeek6ApplicationTests.java b/src/test/java/com/sopt/server/seminar_week6/SeminarWeek6ApplicationTests.java new file mode 100644 index 0000000..5d634fa --- /dev/null +++ b/src/test/java/com/sopt/server/seminar_week6/SeminarWeek6ApplicationTests.java @@ -0,0 +1,13 @@ +package com.sopt.server.seminar_week6; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SeminarWeek6ApplicationTests { + + @Test + void contextLoads() { + } + +}