diff --git a/Assignments/README.md b/Assignments/README.md deleted file mode 100644 index c38edd6..0000000 --- a/Assignments/README.md +++ /dev/null @@ -1 +0,0 @@ -# KimDongHwi \ No newline at end of file diff --git a/ThirdSeminar/.gitignore b/ThirdSeminar/.gitignore index c2065bc..4461634 100644 --- a/ThirdSeminar/.gitignore +++ b/ThirdSeminar/.gitignore @@ -1,6 +1,7 @@ HELP.md .gradle build/ +src/main/resources/application.yml !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -25,6 +26,9 @@ bin/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +*.yml +*.yaml +application.properties ### NetBeans ### /nbproject/private/ diff --git a/ThirdSeminar/build.gradle b/ThirdSeminar/build.gradle index ba82953..0219e97 100644 --- a/ThirdSeminar/build.gradle +++ b/ThirdSeminar/build.gradle @@ -20,9 +20,25 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // Health Check + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + //JWT + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' + implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' + implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' + + implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE' + + //swagger + implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' } tasks.named('test') { diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/ThirdSeminarApplication.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/ThirdSeminarApplication.java index e152738..6170052 100644 --- a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/ThirdSeminarApplication.java +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/ThirdSeminarApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class ThirdSeminarApplication { diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/common/advice/ControllerExceptionAdvice.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/common/advice/ControllerExceptionAdvice.java new file mode 100644 index 0000000..0981b88 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/common/advice/ControllerExceptionAdvice.java @@ -0,0 +1,48 @@ +package sopt.org.ThirdSeminar.common.advice; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import sopt.org.ThirdSeminar.common.dto.ApiResponseDto; +import sopt.org.ThirdSeminar.exception.ErrorStatus; +import sopt.org.ThirdSeminar.exception.model.SoptException; + +import java.util.Objects; + +@RestControllerAdvice +public class ControllerExceptionAdvice { + + /** + * 400 BAD_REQUEST + */ + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ApiResponseDto handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + FieldError fieldError = Objects.requireNonNull(e.getFieldError()); + return ApiResponseDto.error(ErrorStatus.REQUEST_VALIDATION_EXCEPTION, String.format("%s. (%s)", fieldError.getDefaultMessage(), fieldError.getField())); + } + + /** + * 500 Internal Server + */ + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + protected ApiResponseDto handleException(final Exception e) { + e.printStackTrace(); + return ApiResponseDto.error(ErrorStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Sopt custom error + */ + @ExceptionHandler(SoptException.class) + protected ResponseEntity handleSoptException(SoptException e) { + return ResponseEntity.status(e.getHttpStatus()) + .body(ApiResponseDto.error(e.getError(), e.getMessage())); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/common/dto/ApiResponseDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/common/dto/ApiResponseDto.java new file mode 100644 index 0000000..9cbb419 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/common/dto/ApiResponseDto.java @@ -0,0 +1,33 @@ +package sopt.org.ThirdSeminar.common.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import sopt.org.ThirdSeminar.exception.ErrorStatus; +import sopt.org.ThirdSeminar.exception.SuccessStatus; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApiResponseDto { + private final int code; + private final String message; + private T data; + + public static ApiResponseDto success(SuccessStatus successStatus) { + return new ApiResponseDto<>(successStatus.getHttpStatus().value(), successStatus.getMessage()); + } + + public static ApiResponseDto success(SuccessStatus successStatus, T data) { + return new ApiResponseDto(successStatus.getHttpStatus().value(), successStatus.getMessage(), data); + } + + public static ApiResponseDto error(ErrorStatus errorStatus) { + return new ApiResponseDto<>(errorStatus.getHttpStatus().value(), errorStatus.getMessage()); + } + + public static ApiResponseDto error(ErrorStatus errorStatus, String message) { + return new ApiResponseDto<>(errorStatus.getHttpStatusCode(), message); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/WebConfig.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/WebConfig.java new file mode 100644 index 0000000..38a1e84 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/WebConfig.java @@ -0,0 +1,21 @@ +package sopt.org.ThirdSeminar.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import sopt.org.ThirdSeminar.config.resolver.UserIdResolver; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final UserIdResolver userIdResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdResolver); + } + +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/jwt/JwtService.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/jwt/JwtService.java new file mode 100644 index 0000000..c309df3 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/jwt/JwtService.java @@ -0,0 +1,81 @@ +package sopt.org.ThirdSeminar.config.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import sopt.org.ThirdSeminar.exception.ErrorStatus; +import sopt.org.ThirdSeminar.exception.model.UnauthorizedException; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +@Service +public class JwtService { + @Value("${jwt.secret}") + private String jwtSecret; + + @PostConstruct + protected void init() { + jwtSecret = Base64.getEncoder() + .encodeToString(jwtSecret.getBytes(StandardCharsets.UTF_8)); + } + + // JWT 토큰 발급 + public String issuedToken(String userId) { + final Date now = new Date(); + + // 클레임 생성 + final Claims claims = Jwts.claims() + .setSubject("access_token") + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 120 * 60 * 1000L)); + + //private claim 등록 + claims.put("userId", userId); + + return Jwts.builder() + .setHeaderParam(Header.TYPE , Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private Key getSigningKey() { + final byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } + + // JWT 토큰 검증 + public boolean verifyToken(String token) { + try { + final Claims claims = getBody(token); + return true; + } catch (RuntimeException e) { + if (e instanceof ExpiredJwtException) { + throw new UnauthorizedException(ErrorStatus.TOKEN_TIME_EXPIRED_EXCEPTION, ErrorStatus.TOKEN_TIME_EXPIRED_EXCEPTION.getMessage()); + } + return false; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + // JWT 토큰 내용 확인 + public String getJwtContents(String token) { + final Claims claims = getBody(token); + return (String) claims.get("userId"); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/resolver/UserId.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/resolver/UserId.java new file mode 100644 index 0000000..3f6089b --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/resolver/UserId.java @@ -0,0 +1,11 @@ +package sopt.org.ThirdSeminar.config.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserId { +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/resolver/UserIdResolver.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/resolver/UserIdResolver.java new file mode 100644 index 0000000..e2a0f75 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/resolver/UserIdResolver.java @@ -0,0 +1,43 @@ +package sopt.org.ThirdSeminar.config.resolver; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import sopt.org.ThirdSeminar.config.jwt.JwtService; + +import javax.servlet.http.HttpServletRequest; + +@RequiredArgsConstructor +@Component +public class UserIdResolver implements HandlerMethodArgumentResolver { + private final JwtService jwtService; + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserId.class) && Long.class.equals(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + final String token = request.getHeader("Authorization").split(" ")[1]; + + // 토큰 검증 + if (!jwtService.verifyToken(token)) { + throw new RuntimeException(String.format("USER_ID를 가져오지 못했습니다. (%s - %s)", parameter.getClass(), parameter.getMethod())); + } + + // 유저 아이디 반환 + final String tokenContents = jwtService.getJwtContents(token); + try { + return Long.parseLong(tokenContents); + } catch (NumberFormatException e) { + throw new RuntimeException(String.format("USER_ID를 가져오지 못했습니다. (%s - %s)", parameter.getClass(), parameter.getMethod())); + } + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/swagger/SwaggerConfig.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/swagger/SwaggerConfig.java new file mode 100644 index 0000000..a7bdbfb --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/config/swagger/SwaggerConfig.java @@ -0,0 +1,31 @@ +package sopt.org.ThirdSeminar.config.swagger; + +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@SecurityScheme( + name = "JWT Auth", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + Info info = new Info() + .title("SOPT32nd Seminar project") + .description("SOPT32nd Seminar project API Document") + .version("1.0.0"); + + return new OpenAPI() + .components(new Components()) + .info(info); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/BoardController.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/BoardController.java new file mode 100644 index 0000000..dfeacfb --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/BoardController.java @@ -0,0 +1,39 @@ +package sopt.org.ThirdSeminar.controller; + + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import sopt.org.ThirdSeminar.common.dto.ApiResponseDto; +import sopt.org.ThirdSeminar.config.jwt.JwtService; +import sopt.org.ThirdSeminar.config.resolver.UserId; +import sopt.org.ThirdSeminar.controller.dto.request.BoardImageListRequestDto; +import sopt.org.ThirdSeminar.exception.SuccessStatus; +import sopt.org.ThirdSeminar.external.client.aws.S3Service; +import sopt.org.ThirdSeminar.service.BoardService; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/board") +@SecurityRequirement(name = "JWT Auth") +public class BoardController { + + private final BoardService boardService; + private final JwtService jwtService; + private final S3Service s3Service; + + @PostMapping(value = "/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.CREATED) + public ApiResponseDto create( + @UserId Long userId, + @ModelAttribute @Valid final BoardImageListRequestDto request) { + List boardThumbnailImageUrlList = s3Service.uploadImages(request.getBoardImages(), "board"); + boardService.create(userId, boardThumbnailImageUrlList, request); + return ApiResponseDto.success(SuccessStatus.CREATE_BOARD_SUCCESS); + } +} \ No newline at end of file diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/UserController.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/UserController.java new file mode 100644 index 0000000..65cfb0f --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/UserController.java @@ -0,0 +1,44 @@ +package sopt.org.ThirdSeminar.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import sopt.org.ThirdSeminar.common.dto.ApiResponseDto; +import sopt.org.ThirdSeminar.config.jwt.JwtService; +import sopt.org.ThirdSeminar.controller.dto.request.UserLoginRequestDto; +import sopt.org.ThirdSeminar.controller.dto.request.UserRequestDto; +import sopt.org.ThirdSeminar.controller.dto.response.UserLoginResponseDto; +import sopt.org.ThirdSeminar.controller.dto.response.UserResponseDto; +import sopt.org.ThirdSeminar.exception.SuccessStatus; +import sopt.org.ThirdSeminar.service.UserService; + +import javax.validation.Valid; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/user") +@Tag(name = "User", description = "유저 API Document") +public class UserController { + + private final UserService userService; + private final JwtService jwtService; + + @PostMapping("/signup") + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "유저 생성 API", description = "유저를 서버에 등록합니다.") + public ApiResponseDto create(@RequestBody @Valid final UserRequestDto request) { + return ApiResponseDto.success(SuccessStatus.SIGNUP_SUCCESS, userService.create(request)); + } + + @PostMapping("/login") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "유저 로그인 API", description = "유저가 서버에 로그인을 요청합니다.") + public ApiResponseDto login(@RequestBody @Valid final UserLoginRequestDto request) { + final Long userId = userService.login(request); + final String token = jwtService.issuedToken(String.valueOf(userId)); + return ApiResponseDto.success(SuccessStatus.LOGIN_SUCCESS, UserLoginResponseDto.of(userId, token)); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/BoardImageListRequestDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/BoardImageListRequestDto.java new file mode 100644 index 0000000..bf14a3c --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/BoardImageListRequestDto.java @@ -0,0 +1,25 @@ +package sopt.org.ThirdSeminar.controller.dto.request; + + +import com.sun.istack.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Getter +@Setter +public class BoardImageListRequestDto { + private List boardImages; + + @NotBlank + private String title; + + @NotBlank + private String content; + + @NotNull + private Boolean isPublic; +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/BoardRequestDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/BoardRequestDto.java new file mode 100644 index 0000000..a822a3e --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/BoardRequestDto.java @@ -0,0 +1,30 @@ +package sopt.org.ThirdSeminar.controller.dto.request; + +import com.sun.istack.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class BoardRequestDto { +// @Email(message = "이메일 형식에 맞지 않습니다") +// private String email; + + @NotNull + private MultipartFile thumbnail; + + @NotBlank + private String title; + + @NotBlank + private String content; + + @NotNull + private Boolean isPublic; +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/UserLoginRequestDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/UserLoginRequestDto.java new file mode 100644 index 0000000..6e818d8 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/UserLoginRequestDto.java @@ -0,0 +1,25 @@ +package sopt.org.ThirdSeminar.controller.dto.request; + +import com.sun.istack.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserLoginRequestDto { + @Email(message = "이메일 형식에 맞지 않습니다") + @NotBlank + private String email; + + @NotNull + @Pattern( + regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다" + ) + private String password; +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/UserRequestDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/UserRequestDto.java new file mode 100644 index 0000000..665532f --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/request/UserRequestDto.java @@ -0,0 +1,34 @@ +package sopt.org.ThirdSeminar.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(description = "유저 생성 DTO") +public class UserRequestDto { + @Email(message = "이메일 형식에 맞지 않습니다") + @NotNull + @Schema(description = "유저 이메일") + private String email; + + @NotBlank + @Pattern(regexp = "^[가-힣a-zA-Z]{2,10}$", message = "닉네임 형식에 맞지 않습니다.") //정규 표현식, 데이터 검증 + @Schema(description = "유저 닉네임") + private String nickname; + + @NotBlank + @Pattern( + regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다." + ) + @Schema(description = "유저 비밀번호") + private String password; +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/response/UserLoginResponseDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/response/UserLoginResponseDto.java new file mode 100644 index 0000000..8a567ea --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/response/UserLoginResponseDto.java @@ -0,0 +1,18 @@ +package sopt.org.ThirdSeminar.controller.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserLoginResponseDto { + private Long userId; + private String accessToken; + + public static UserLoginResponseDto of(Long userId, String accessToken) { + return new UserLoginResponseDto(userId, accessToken); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/response/UserResponseDto.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/response/UserResponseDto.java new file mode 100644 index 0000000..b83c5b1 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/controller/dto/response/UserResponseDto.java @@ -0,0 +1,19 @@ +package sopt.org.ThirdSeminar.controller.dto.response; + + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserResponseDto { + private Long userId; + private String nickname; + + public static UserResponseDto of(Long userId, String nickname) { + return new UserResponseDto(userId, nickname); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/AuditingTimeEntity.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/AuditingTimeEntity.java new file mode 100644 index 0000000..ee5ce88 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/AuditingTimeEntity.java @@ -0,0 +1,22 @@ +package sopt.org.ThirdSeminar.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.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AuditingTimeEntity { //혼자서는 크게 의미가 없는 class이기에 엔티티에 상속받아서 사용하라고 추상클래스로 만듦 + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/Board.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/Board.java new file mode 100644 index 0000000..2106c60 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/Board.java @@ -0,0 +1,44 @@ +package sopt.org.ThirdSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Board extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT)) + private User user; + + @Column + private String thumbnail; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private Boolean isPublic; + + private Board(User user, String title, String content, Boolean isPublic) { + this.user = user; + this.title =title; + this.content = content; + this.isPublic = isPublic; + } + + public static Board newInstance(User user, String title, String content, Boolean isPublic) { + return new Board(user, title, content, isPublic); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/Image.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/Image.java new file mode 100644 index 0000000..eb81da1 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/Image.java @@ -0,0 +1,32 @@ +package sopt.org.ThirdSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Image extends AuditingTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT)) + private Board board; + + @Column(nullable = false) + private String imageUrl; + + private Image(Board board, String imageUrl) { + this.board = board; + this.imageUrl = imageUrl; + } + + public static Image newInstance(Board board, String imageUrl) { + return new Image(board, imageUrl); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/User.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/User.java new file mode 100644 index 0000000..49db036 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/domain/User.java @@ -0,0 +1,31 @@ +package sopt.org.ThirdSeminar.domain; + + +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends AuditingTimeEntity{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @Builder + public User(String nickname, String email, String password) { + this.nickname = nickname; + this.email = email; + this.password = password; + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/ErrorStatus.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/ErrorStatus.java new file mode 100644 index 0000000..a86a282 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/ErrorStatus.java @@ -0,0 +1,59 @@ +package sopt.org.ThirdSeminar.exception; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum ErrorStatus { + /* + BAD_REQUEST + */ + VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + VALIDATION_REQUEST_MISSING_EXCEPTION(HttpStatus.BAD_REQUEST, "요청값이 입력되지 않았습니다."), + INVALID_PASSWORD_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 비밀번호 입니다."), + + + /** + * 400 BAD REQUEST + */ + REQUEST_VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"), + + TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), + + /* + CONFLICT + */ + CONFLICT_EMAIL_EXCEPTION(HttpStatus.CONFLICT, "이미 등록된 이메일입니다."), + CONFLICT_NICKNAME_EXCEPTION(HttpStatus.CONFLICT, "이미 등록된 닉네임입니다."), + INVALID_MULTIPART_EXTENSION_EXCEPTION(HttpStatus.CONFLICT, "유효하지 않은 파일입니다."), + + /** + * 409 CONFLICT + */ + ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"), + + /** + * 404 NOT FOUND + */ + NOT_FOUND_USER_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다"), + NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지가 존재하지 않습니다."), + NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "저장된 이미지가 존재하지 않습니다."), + + /* + SERVER_ERROR + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "예상치 못한 서버 에러가 발생했습니다."), + BAD_GATEWAY_EXCEPTION(HttpStatus.BAD_GATEWAY, "일시적인 에러가 발생하였습니다.\n잠시 후 다시 시도해주세요!"), + SERVICE_UNAVAILABLE_EXCEPTION(HttpStatus.SERVICE_UNAVAILABLE, "현재 점검 중입니다.\n잠시 후 다시 시도해주세요!"), + ; + + private final HttpStatus httpStatus; + private final String message; + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/SuccessStatus.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/SuccessStatus.java new file mode 100644 index 0000000..a3b1f01 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/SuccessStatus.java @@ -0,0 +1,25 @@ +package sopt.org.ThirdSeminar.exception; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum SuccessStatus { + /** + * 201 CREATED + */ + SIGNUP_SUCCESS(HttpStatus.CREATED, "회원가입이 완료됐습니다."), + CREATE_BOARD_SUCCESS(HttpStatus.CREATED, "게시물 생성이 완료됐습니다."), + LOGIN_SUCCESS(HttpStatus.CREATED, "로그인을 성공했습니다."), + ; + + private final HttpStatus httpStatus; + private final String message; + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/BadRequestException.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/BadRequestException.java new file mode 100644 index 0000000..76c697f --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/BadRequestException.java @@ -0,0 +1,9 @@ +package sopt.org.ThirdSeminar.exception.model; + +import sopt.org.ThirdSeminar.exception.ErrorStatus; + +public class BadRequestException extends SoptException { + public BadRequestException(ErrorStatus error, String message) { + super(error, message); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/ConflictException.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/ConflictException.java new file mode 100644 index 0000000..f31778d --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/ConflictException.java @@ -0,0 +1,9 @@ +package sopt.org.ThirdSeminar.exception.model; + +import sopt.org.ThirdSeminar.exception.ErrorStatus; + +public class ConflictException extends SoptException { + public ConflictException(ErrorStatus error, String message) { + super(error, message); + } +} \ No newline at end of file diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/NotFoundException.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/NotFoundException.java new file mode 100644 index 0000000..8779904 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/NotFoundException.java @@ -0,0 +1,9 @@ +package sopt.org.ThirdSeminar.exception.model; + +import sopt.org.ThirdSeminar.exception.ErrorStatus; + +public class NotFoundException extends SoptException { + public NotFoundException(ErrorStatus error, String message) { + super(error, message); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/SoptException.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/SoptException.java new file mode 100644 index 0000000..ab168dc --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/SoptException.java @@ -0,0 +1,19 @@ +package sopt.org.ThirdSeminar.exception.model; + +import lombok.Getter; +import sopt.org.ThirdSeminar.exception.ErrorStatus; + +@Getter +public class SoptException extends RuntimeException { + + private final ErrorStatus error; + + public SoptException(ErrorStatus error, String message) { + super(message); + this.error = error; + } + + public int getHttpStatus() { + return error.getHttpStatusCode(); + } +} \ No newline at end of file diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/UnauthorizedException.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/UnauthorizedException.java new file mode 100644 index 0000000..36067c9 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/exception/model/UnauthorizedException.java @@ -0,0 +1,9 @@ +package sopt.org.ThirdSeminar.exception.model; + +import sopt.org.ThirdSeminar.exception.ErrorStatus; + +public class UnauthorizedException extends SoptException{ + public UnauthorizedException(ErrorStatus error, String message) { + super(error, message); + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/external/client/aws/S3Service.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/external/client/aws/S3Service.java new file mode 100644 index 0000000..95d97ec --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/external/client/aws/S3Service.java @@ -0,0 +1,104 @@ +package sopt.org.ThirdSeminar.external.client.aws; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import sopt.org.ThirdSeminar.exception.ErrorStatus; +import sopt.org.ThirdSeminar.exception.model.BadRequestException; +import sopt.org.ThirdSeminar.exception.model.NotFoundException; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class S3Service { + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + @PostConstruct + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + public String uploadImage(MultipartFile multipartFile, String folder) { + String fileName = createFileName(multipartFile.getOriginalFilename()); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + + try(InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket+"/"+ folder + "/image", fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return amazonS3.getUrl(bucket+"/"+ folder + "/image", fileName).toString(); + } catch(IOException e) { + throw new NotFoundException(ErrorStatus.NOT_FOUND_SAVE_IMAGE_EXCEPTION, ErrorStatus.NOT_FOUND_SAVE_IMAGE_EXCEPTION.getMessage()); + } + } + + public List uploadImages(List multipartFileList, String folder) { + List result = new ArrayList<>(); + multipartFileList.forEach(multipartFile -> { + result.add(uploadImage(multipartFile, folder)); + }); + return result; + } + + // 파일명 (중복 방지) + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + // 파일 유효성 검사 + private String getFileExtension(String fileName) { + if (fileName.length() == 0) { + throw new NotFoundException(ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION, ErrorStatus.NOT_FOUND_USER_EXCEPTION.getMessage()); + } + ArrayList fileValidate = new ArrayList<>(); + fileValidate.add(".jpg"); + fileValidate.add(".jpeg"); + fileValidate.add(".png"); + fileValidate.add(".JPG"); + fileValidate.add(".JPEG"); + fileValidate.add(".PNG"); + String idxFileName = fileName.substring(fileName.lastIndexOf(".")); + if (!fileValidate.contains(idxFileName)) { + throw new BadRequestException(ErrorStatus.INVALID_MULTIPART_EXTENSION_EXCEPTION, ErrorStatus.INVALID_MULTIPART_EXTENSION_EXCEPTION.getMessage()); + } + return fileName.substring(fileName.lastIndexOf(".")); + } + + //삭제 + public void deleteFile(String imageUrl) { + String imageKey = imageUrl.substring(56); + amazonS3.deleteObject(bucket, imageKey); + } +} \ No newline at end of file diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/BoardRepository.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/BoardRepository.java new file mode 100644 index 0000000..9052fe6 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/BoardRepository.java @@ -0,0 +1,16 @@ +package sopt.org.ThirdSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.ThirdSeminar.domain.Board; + +public interface BoardRepository extends Repository { + + // CREATE + void save(Board board); + + // READ + + // UPDATE + + // DELETE +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/ImageRepository.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/ImageRepository.java new file mode 100644 index 0000000..05cb13d --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/ImageRepository.java @@ -0,0 +1,8 @@ +package sopt.org.ThirdSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.ThirdSeminar.domain.Image; + +public interface ImageRepository extends Repository { + void save(Image image); +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/UserRepository.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/UserRepository.java new file mode 100644 index 0000000..98034e7 --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/infrastructure/UserRepository.java @@ -0,0 +1,22 @@ +package sopt.org.ThirdSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.ThirdSeminar.domain.User; + +import java.util.Optional; + +public interface UserRepository extends Repository { + //CREATE + void save(User user); + + //READ + Optional findByEmail(String email); + + boolean existsByEmail(String email); + + Optional findById(Long id); + + //UPDATE + + //DELETE +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/service/BoardService.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/service/BoardService.java new file mode 100644 index 0000000..4b5429f --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/service/BoardService.java @@ -0,0 +1,43 @@ +package sopt.org.ThirdSeminar.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import sopt.org.ThirdSeminar.controller.dto.request.BoardImageListRequestDto; +import sopt.org.ThirdSeminar.domain.Board; +import sopt.org.ThirdSeminar.domain.Image; +import sopt.org.ThirdSeminar.domain.User; +import sopt.org.ThirdSeminar.exception.ErrorStatus; +import sopt.org.ThirdSeminar.exception.model.NotFoundException; +import sopt.org.ThirdSeminar.infrastructure.BoardRepository; +import sopt.org.ThirdSeminar.infrastructure.ImageRepository; +import sopt.org.ThirdSeminar.infrastructure.UserRepository; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BoardService { + private final UserRepository userRepository; + private final BoardRepository boardRepository; + private final ImageRepository imageRepository; + + @Transactional + public void create(Long userId, List boardImageUrlList, BoardImageListRequestDto request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.NOT_FOUND_USER_EXCEPTION, ErrorStatus.NOT_FOUND_USER_EXCEPTION.getMessage())); + + Board newBoard = Board.newInstance( + user, + request.getTitle(), + request.getContent(), + request.getIsPublic() + ); + + boardRepository.save(newBoard); + + for (String boardImageUrl : boardImageUrlList) { + imageRepository.save(Image.newInstance(newBoard, boardImageUrl)); + } + } +} diff --git a/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/service/UserService.java b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/service/UserService.java new file mode 100644 index 0000000..bdca27c --- /dev/null +++ b/ThirdSeminar/src/main/java/sopt/org/ThirdSeminar/service/UserService.java @@ -0,0 +1,48 @@ +package sopt.org.ThirdSeminar.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.ThirdSeminar.controller.dto.request.UserLoginRequestDto; +import sopt.org.ThirdSeminar.controller.dto.request.UserRequestDto; +import sopt.org.ThirdSeminar.controller.dto.response.UserResponseDto; +import sopt.org.ThirdSeminar.domain.User; +import sopt.org.ThirdSeminar.exception.ErrorStatus; +import sopt.org.ThirdSeminar.exception.model.BadRequestException; +import sopt.org.ThirdSeminar.exception.model.ConflictException; +import sopt.org.ThirdSeminar.exception.model.NotFoundException; +import sopt.org.ThirdSeminar.infrastructure.UserRepository; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + + @Transactional + public UserResponseDto create(UserRequestDto request) { + if (userRepository.existsByEmail(request.getEmail())) { + throw new ConflictException(ErrorStatus.ALREADY_EXIST_USER_EXCEPTION, ErrorStatus.ALREADY_EXIST_USER_EXCEPTION.getMessage()); + } + User user = User.builder() + .email(request.getEmail()) + .nickname(request.getNickname()) + .password(request.getPassword()) + .build(); + + userRepository.save(user); + + return UserResponseDto.of(user.getId(), user.getNickname()); + } + + @Transactional + public Long login(UserLoginRequestDto request) { + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new NotFoundException(ErrorStatus.NOT_FOUND_USER_EXCEPTION, ErrorStatus.NOT_FOUND_USER_EXCEPTION.getMessage())); + + if (!user.getPassword().equals(request.getPassword())) { + throw new BadRequestException(ErrorStatus.INVALID_PASSWORD_EXCEPTION, ErrorStatus.INVALID_PASSWORD_EXCEPTION.getMessage()); + } + + return user.getId(); + } +} diff --git a/ThirdSeminar/src/main/resources/application.properties b/ThirdSeminar/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/ThirdSeminar/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -