diff --git a/.gitignore b/.gitignore index 86cd5cb..0cdd459 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,12 @@ build/ *.iws *.iml *.ipr +src/main/resources/application.yml +application.yml out/ !**/src/main/**/out/ !**/src/test/**/out/ +src/main/resources/logback.xml ### Eclipse ### .apt_generated diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 43cc8e4..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index f6589e3..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index f7bab39..38bbebc 100644 --- a/build.gradle +++ b/build.gradle @@ -22,10 +22,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'mysql:mysql-connector-java:8.0.32' testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/org/sopt/diary/api/DiaryController.java b/src/main/java/org/sopt/diary/api/DiaryController.java deleted file mode 100644 index 1773a17..0000000 --- a/src/main/java/org/sopt/diary/api/DiaryController.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.sopt.diary.api; - -import org.sopt.diary.api.dto.req.DiaryEditReq; -import org.sopt.diary.api.dto.req.DiaryPostReq; -import org.sopt.diary.api.dto.res.DiaryDetailInfoRes; -import org.sopt.diary.api.dto.res.DiaryListRes; -import org.sopt.diary.common.Constants; -import org.sopt.diary.common.util.ValidatorUtil; -import org.sopt.diary.service.DiaryService; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - - -@RestController -public class DiaryController { - private final DiaryService diaryService; - - public DiaryController(final DiaryService diaryService) { - this.diaryService = diaryService; - } - - //일기 작성 API - @PostMapping("/diary") - ResponseEntity postDiary(@RequestBody final DiaryPostReq diaryPostReq) { - ValidatorUtil.validStringLength(diaryPostReq.content(), Constants.MAX_CONTENT_LENGTH); - diaryService.createDiary(diaryPostReq); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - - @GetMapping("/diaries") - ResponseEntity getDiaryList() { - final DiaryListRes diaryList = diaryService.getDiaryList(); - return ResponseEntity.status(HttpStatus.OK).body(diaryList); - } - - @GetMapping("/diary/{id}") - ResponseEntity getDiaryDetailInfo(@PathVariable final Long id) { - final DiaryDetailInfoRes diaryDetailInfoRes = diaryService.getDiaryDetailInfo(id); - return ResponseEntity.status(HttpStatus.OK).body(diaryDetailInfoRes); - } - - @PatchMapping("/diary/{id}") - ResponseEntity editDiaryContent(@PathVariable final Long id, - @RequestBody final DiaryEditReq diaryEditReq) { - ValidatorUtil.validStringLength(diaryEditReq.content(), Constants.MAX_CONTENT_LENGTH); - diaryService.editDiaryContent(id, diaryEditReq); - return ResponseEntity.status(HttpStatus.OK).build(); - } - - @DeleteMapping("diary/{id}") - ResponseEntity deleteDiary(@PathVariable final Long id) { - diaryService.deleteDiary(id); - return ResponseEntity.status(HttpStatus.OK).build(); - } -} diff --git a/src/main/java/org/sopt/diary/api/dto/req/DiaryEditReq.java b/src/main/java/org/sopt/diary/api/dto/req/DiaryEditReq.java deleted file mode 100644 index 40ff2c6..0000000 --- a/src/main/java/org/sopt/diary/api/dto/req/DiaryEditReq.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.sopt.diary.api.dto.req; - -public record DiaryEditReq( - String content -) { -} diff --git a/src/main/java/org/sopt/diary/api/dto/req/DiaryPostReq.java b/src/main/java/org/sopt/diary/api/dto/req/DiaryPostReq.java deleted file mode 100644 index 9e49d5f..0000000 --- a/src/main/java/org/sopt/diary/api/dto/req/DiaryPostReq.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.sopt.diary.api.dto.req; - -public record DiaryPostReq( - String title, - String content) { -} diff --git a/src/main/java/org/sopt/diary/api/dto/res/DiaryListRes.java b/src/main/java/org/sopt/diary/api/dto/res/DiaryListRes.java deleted file mode 100644 index 374cfdb..0000000 --- a/src/main/java/org/sopt/diary/api/dto/res/DiaryListRes.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sopt.diary.api.dto.res; - -import java.util.List; - -public record DiaryListRes( - List diaryList -) { - public static DiaryListRes of(final List diaryIdAndTitle) { - return new DiaryListRes(diaryIdAndTitle); - } - - public record DiaryIdAndTitle( - Long id, - String title - ) { - public static DiaryIdAndTitle of(final Long id, final String title) { - return new DiaryIdAndTitle(id, title); - } - } -} diff --git a/src/main/java/org/sopt/diary/common/Failure/CommonFailureInfo.java b/src/main/java/org/sopt/diary/common/Failure/CommonFailureInfo.java index e9346d2..f99838e 100644 --- a/src/main/java/org/sopt/diary/common/Failure/CommonFailureInfo.java +++ b/src/main/java/org/sopt/diary/common/Failure/CommonFailureInfo.java @@ -8,6 +8,13 @@ public enum CommonFailureInfo implements FailureCode{ * 400 Bad Reqeust */ INVALID_INPUT(HttpStatus.BAD_REQUEST, "잘못된 요청값입니다."), + MISSING_REQUEST_HEADER(HttpStatus.BAD_REQUEST, "필요한 헤더값이 없습니다."), + INVALID_HEADER_TYPE(HttpStatus.BAD_REQUEST, "의 타입이 잘못되었습니다."), + MISSING_REQUEST_PARAM(HttpStatus.BAD_REQUEST, "파라미터값이 없습니다."), + INVALID_END_POINT(HttpStatus.BAD_REQUEST, "잘못된 엔드포인트 접근입니다."), + ALREADY_EXITST_TITLE(HttpStatus.CONFLICT, "이미 존재하는 제목입니다."), + + ; private final HttpStatus status; diff --git a/src/main/java/org/sopt/diary/common/Failure/DiaryFailureInfo.java b/src/main/java/org/sopt/diary/common/Failure/DiaryFailureInfo.java index 07b8b2a..18c0e56 100644 --- a/src/main/java/org/sopt/diary/common/Failure/DiaryFailureInfo.java +++ b/src/main/java/org/sopt/diary/common/Failure/DiaryFailureInfo.java @@ -15,6 +15,8 @@ public enum DiaryFailureInfo implements FailureCode { DIARY_NOT_FOUND(HttpStatus.NOT_FOUND, "일기를 찾을 수 없습니다."), EMPTY_DIARY(HttpStatus.NOT_FOUND, "현재 작성된 일기가 없습니다."), + UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED, "접근 권한이 없습니다"), + ; private final HttpStatus status; diff --git a/src/main/java/org/sopt/diary/common/Failure/FailureResponse.java b/src/main/java/org/sopt/diary/common/Failure/FailureResponse.java index fd93b96..4b2ba4b 100644 --- a/src/main/java/org/sopt/diary/common/Failure/FailureResponse.java +++ b/src/main/java/org/sopt/diary/common/Failure/FailureResponse.java @@ -1,10 +1,18 @@ package org.sopt.diary.common.Failure; +import org.springframework.http.HttpStatus; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + public record FailureResponse( int status, String message ) { - public static FailureResponse of(FailureCode failureCode) { + + public static FailureResponse of(final FailureCode failureCode) { return new FailureResponse(failureCode.getStatus().value(), failureCode.getMessage()); } + + public static FailureResponse of(final HttpStatus status, final String message) { + return new FailureResponse(status.value(), message); + } } diff --git a/src/main/java/org/sopt/diary/common/Failure/UserFailureInfo.java b/src/main/java/org/sopt/diary/common/Failure/UserFailureInfo.java new file mode 100644 index 0000000..61a0119 --- /dev/null +++ b/src/main/java/org/sopt/diary/common/Failure/UserFailureInfo.java @@ -0,0 +1,29 @@ +package org.sopt.diary.common.Failure; + + +import org.springframework.http.HttpStatus; + +public enum UserFailureInfo implements FailureCode { + + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "없는 유저입니다"), + INVALID_USER_PASSWROD(HttpStatus.BAD_REQUEST, "잘못된 비밀번호입니다."), + + ; + + private final HttpStatus status; + private final String message; + + UserFailureInfo(final HttpStatus status, final String message) { + this.status = status; + this.message = message; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + +} \ No newline at end of file diff --git a/src/main/java/org/sopt/diary/common/GlobalExceptionHandler.java b/src/main/java/org/sopt/diary/common/GlobalExceptionHandler.java index 64779ce..7491e7e 100644 --- a/src/main/java/org/sopt/diary/common/GlobalExceptionHandler.java +++ b/src/main/java/org/sopt/diary/common/GlobalExceptionHandler.java @@ -1,14 +1,32 @@ package org.sopt.diary.common; +import jakarta.validation.UnexpectedTypeException; +import jakarta.validation.ValidationException; import org.sopt.diary.common.Failure.CommonFailureInfo; -import org.sopt.diary.common.Failure.DiaryFailureInfo; import org.sopt.diary.common.Failure.FailureResponse; +import org.sopt.diary.common.enums.validation.ValidationError; import org.sopt.diary.exception.BusinessException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.resource.NoResourceFoundException; +import java.util.List; + +import static org.sopt.diary.common.enums.validation.DefaultErrorMessage.getDefaultFromHandlerMethodValidationException; + +//<컨트롤러 가기전 예외, 컨트롤러에서의 예외, 그 후의 예외들> 이렇게 모아둘까..? 예외메세지 줄 떄 어느정도까지 줘야되는지도 궁금함 +//boolean은 어떻게 검증함? boolean 필드에 아무값이나 null넣거나 숫자 이상한거 넣어도 boolean이 알아서 들어감... +//String값이 아닌거 넣어도 String으로 알아서 들어가는거같음(포맨이라그런가) @RestControllerAdvice public class GlobalExceptionHandler { @@ -17,8 +35,81 @@ protected ResponseEntity handleBusinessException(final Business return ResponseEntity.status(e.getFailureCode().getStatus()).body(FailureResponse.of(e.getFailureCode())); } - @ExceptionHandler(IllegalArgumentException.class) - protected ResponseEntity handleIllegalArgumentException(final IllegalArgumentException e) { + @ExceptionHandler(HttpMessageNotReadableException.class) + protected ResponseEntity handleHttpMessageNotReadableException(final HttpMessageNotReadableException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.INVALID_INPUT)); + } + + //@Valid 예외처리 (BindingResult) + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + List errors = ValidationError.of(e.getBindingResult()); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(HttpStatus.BAD_REQUEST, errors.toString())); + } + + //헤더 없을 때 예외처리 + @ExceptionHandler(MissingRequestHeaderException.class) + protected ResponseEntity handleMissingRequestHeaderException(MissingRequestHeaderException e) { + System.out.println(e.getHeaderName() + "헤더가 없습니다"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.MISSING_REQUEST_HEADER)); + } + + // 타입 다를때 + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(FailureResponse.of( //여기서 흠 어떤식으로 하는게 좋을까.. 따로 requestFailureInfo 만들어서 거기다가 잘못된 요청값 예외들 모아둘까..? + HttpStatus.BAD_REQUEST, + e.getPropertyName() + CommonFailureInfo.INVALID_HEADER_TYPE.getMessage())); // ex)userId의 타입이 잘못되었습니다. + } + + //스프링 3.2 이후로 @Valid에러 여기서 잡히는듯..? + @ExceptionHandler(HandlerMethodValidationException.class) + protected ResponseEntity handleHandlerMethodValidationException(HandlerMethodValidationException e) { + final String defaultErrorMessage = getDefaultFromHandlerMethodValidationException(e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(HttpStatus.BAD_REQUEST, defaultErrorMessage)); + } + + @ExceptionHandler(UnexpectedTypeException.class) + protected ResponseEntity handleUnexpectedTypeException(UnexpectedTypeException e) { +// System.out.println(e.); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.MISSING_REQUEST_PARAM)); + } + + + +// 현재 enum필드에 아예 필드자체도 안들어갔을때 이 예외가 떠서 일단 이거로 해둠 + @ExceptionHandler(ValidationException.class) + protected ResponseEntity handleValidationException(ValidationException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.INVALID_INPUT)); } + + //request param 없을 때 + @ExceptionHandler(MissingServletRequestParameterException.class) + protected ResponseEntity handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.MISSING_REQUEST_PARAM)); + } + + //httpmethod 잘못 넣거나, 요청값 잘못넣었을떄 + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.INVALID_INPUT)); + } + + //존재하지 않는 엔드포인트로 접근할때 + @ExceptionHandler(NoResourceFoundException.class) + protected ResponseEntity handleNoResourceFoundException(NoResourceFoundException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.INVALID_END_POINT)); + } + + //유니크 키 충돌 + @ExceptionHandler(DataIntegrityViolationException.class) + protected ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(FailureResponse.of(CommonFailureInfo.ALREADY_EXITST_TITLE)); + } + + } diff --git a/src/main/java/org/sopt/diary/common/enums/Category.java b/src/main/java/org/sopt/diary/common/enums/Category.java new file mode 100644 index 0000000..3b26b0e --- /dev/null +++ b/src/main/java/org/sopt/diary/common/enums/Category.java @@ -0,0 +1,10 @@ +package org.sopt.diary.common.enums; + +public enum Category { + FOOD, + SCHOOL, + MOVIE, + EXERCISE, + ALL, + ; +} diff --git a/src/main/java/org/sopt/diary/common/enums/SortBy.java b/src/main/java/org/sopt/diary/common/enums/SortBy.java new file mode 100644 index 0000000..5fe506d --- /dev/null +++ b/src/main/java/org/sopt/diary/common/enums/SortBy.java @@ -0,0 +1,7 @@ +package org.sopt.diary.common.enums; + +public enum SortBy { + LATEST, + QUANTITY, + ; +} diff --git a/src/main/java/org/sopt/diary/common/enums/validation/DefaultErrorMessage.java b/src/main/java/org/sopt/diary/common/enums/validation/DefaultErrorMessage.java new file mode 100644 index 0000000..166d74d --- /dev/null +++ b/src/main/java/org/sopt/diary/common/enums/validation/DefaultErrorMessage.java @@ -0,0 +1,28 @@ +package org.sopt.diary.common.enums.validation; + +import org.springframework.validation.FieldError; +import org.springframework.web.method.annotation.HandlerMethodValidationException; + +import java.util.stream.Collectors; + +public class DefaultErrorMessage { + + //@Size, @Notblank + //메세지들도 가져옴 + public static String getDefaultFromHandlerMethodValidationException(HandlerMethodValidationException e) { + return e.getAllErrors().stream() + .map(error -> { + if (error instanceof FieldError fieldError) { + return String.format("[%s: %s]", + fieldError.getField(), + fieldError.getDefaultMessage()); + } else { + return String.format("[%s]", + error.getDefaultMessage()); + } + }) + .collect(Collectors.joining(" ")); + } + + +} diff --git a/src/main/java/org/sopt/diary/common/enums/validation/EnumValue.java b/src/main/java/org/sopt/diary/common/enums/validation/EnumValue.java new file mode 100644 index 0000000..8ac4ab7 --- /dev/null +++ b/src/main/java/org/sopt/diary/common/enums/validation/EnumValue.java @@ -0,0 +1,27 @@ +package org.sopt.diary.common.enums.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = {ValueOfEnumValidator.class}) +public @interface EnumValue { + String message() default "Enum에 없는 값입니다."; + + Class[] groups() default { }; + + Class[] payload() default { }; + + Class> enumClass(); + + boolean ignoreCase() default false; +} diff --git a/src/main/java/org/sopt/diary/common/enums/validation/ValidationError.java b/src/main/java/org/sopt/diary/common/enums/validation/ValidationError.java new file mode 100644 index 0000000..5ea6cba --- /dev/null +++ b/src/main/java/org/sopt/diary/common/enums/validation/ValidationError.java @@ -0,0 +1,17 @@ +package org.sopt.diary.common.enums.validation; + +import org.sopt.diary.common.GlobalExceptionHandler; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import java.util.List; + +public record ValidationError( + String message) { + + public static List of(final BindingResult bindingResult){ + return bindingResult.getFieldErrors().stream() + .map(fieldError -> new ValidationError(fieldError.getDefaultMessage())) + .toList(); + } +} diff --git a/src/main/java/org/sopt/diary/common/enums/validation/ValueOfEnumValidator.java b/src/main/java/org/sopt/diary/common/enums/validation/ValueOfEnumValidator.java new file mode 100644 index 0000000..ce411dc --- /dev/null +++ b/src/main/java/org/sopt/diary/common/enums/validation/ValueOfEnumValidator.java @@ -0,0 +1,35 @@ +package org.sopt.diary.common.enums.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.ValidationException; +import org.apache.coyote.BadRequestException; +import org.sopt.diary.common.Failure.CommonFailureInfo; +import org.sopt.diary.exception.BusinessException; + +public class ValueOfEnumValidator implements ConstraintValidator { + + private EnumValue annotation; + + @Override + public void initialize(EnumValue constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + //equalsIgnoreCase -> 대소문자를 구분하지 않고 비교 + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value.equals(enumValue.toString()) + || (this.annotation.ignoreCase() && value.equalsIgnoreCase(enumValue.toString()))) { + return true; + } + } + } else { + throw new BusinessException(CommonFailureInfo.INVALID_INPUT); + } + return false; + } +} diff --git a/src/main/java/org/sopt/diary/common/util/BaseTimeEntity.java b/src/main/java/org/sopt/diary/common/util/BaseTimeEntity.java index 95d4116..aabdc00 100644 --- a/src/main/java/org/sopt/diary/common/util/BaseTimeEntity.java +++ b/src/main/java/org/sopt/diary/common/util/BaseTimeEntity.java @@ -1,5 +1,6 @@ package org.sopt.diary.common.util; +import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import org.springframework.data.annotation.CreatedDate; @@ -11,6 +12,7 @@ @EntityListeners(AuditingEntityListener.class) //해당 어노테이션은 엔티티의 변화를 감지하여 엔티티와 매핑된 테이블의 데이터를 조작 public abstract class BaseTimeEntity { @CreatedDate + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm") //형식 지정 private LocalDateTime createdAt; public LocalDateTime getCreatedAt() { diff --git a/src/main/java/org/sopt/diary/common/util/ValidatorUtil.java b/src/main/java/org/sopt/diary/common/util/ValidatorUtil.java index 617f060..ef899f4 100644 --- a/src/main/java/org/sopt/diary/common/util/ValidatorUtil.java +++ b/src/main/java/org/sopt/diary/common/util/ValidatorUtil.java @@ -1,12 +1,22 @@ package org.sopt.diary.common.util; import org.sopt.diary.common.Failure.DiaryFailureInfo; -import org.sopt.diary.exception.BadRequestException; +import org.sopt.diary.exception.BusinessException; + +import java.util.List; public final class ValidatorUtil { public static void validStringLength(final String text, final int length) { if (text.length() > length) { - throw new BadRequestException(DiaryFailureInfo.INVALID_CONTENT_SIZE); + throw new BusinessException(DiaryFailureInfo.INVALID_CONTENT_SIZE); + } + } + + public static boolean isListEmpty(final List list) { + if (list == null || list.isEmpty()) { + return true; + } else { + return false; } } } diff --git a/src/main/java/org/sopt/diary/domain/diary/api/DiaryController.java b/src/main/java/org/sopt/diary/domain/diary/api/DiaryController.java new file mode 100644 index 0000000..6f58968 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/api/DiaryController.java @@ -0,0 +1,93 @@ +package org.sopt.diary.domain.diary.api; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.sopt.diary.common.enums.Category; +import org.sopt.diary.common.enums.SortBy; +import org.sopt.diary.common.enums.validation.EnumValue; +import org.sopt.diary.domain.diary.api.dto.req.DiaryEditReq; +import org.sopt.diary.domain.diary.api.dto.req.DiaryPostReq; +import org.sopt.diary.domain.diary.api.dto.res.DiaryDetailInfoRes; +import org.sopt.diary.domain.diary.api.dto.res.DiaryListRes; +import org.sopt.diary.common.Constants; +import org.sopt.diary.common.util.ValidatorUtil; +import org.sopt.diary.domain.diary.api.dto.res.DiaryMyListRes; +import org.sopt.diary.domain.diary.service.DiaryService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +public class DiaryController { + private final DiaryService diaryService; + + public DiaryController(DiaryService diaryService) { + this.diaryService = diaryService; + } + + //일기 작성 API + @PostMapping("/diary") + ResponseEntity postDiary(@NotNull @Min(1) @RequestHeader("userId") final long userId, + @Valid @RequestBody final DiaryPostReq diaryPostReq) { + ValidatorUtil.validStringLength(diaryPostReq.content(), Constants.MAX_CONTENT_LENGTH); + diaryService.createDiary(userId, diaryPostReq.title(), diaryPostReq.content(), diaryPostReq.category(), diaryPostReq.isPrivate()); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + //전체 일기 목록 조회 API + @GetMapping("/diaries") + ResponseEntity getDiaryList( + @EnumValue(enumClass = Category.class, message = "유효하지 않은 카테고리입니다", ignoreCase = false) + @RequestParam("category") + final String category, + @EnumValue(enumClass = SortBy.class, message = "유효하지 않은 카테고리입니다", ignoreCase = false) + @RequestParam("sort") + final String sort) { + final DiaryListRes diaryList = diaryService.getDiaryList(category, sort); + return ResponseEntity.status(HttpStatus.OK).body(diaryList); + } + + //개인 일기 목록 조회 API + @GetMapping("/diaries/my") + ResponseEntity getMyDiaryList( + @NotNull + @Min(1) + @RequestHeader("userId") + final long userId, + @EnumValue(enumClass = Category.class, message = "유효하지 않은 카테고리입니다", ignoreCase = false) + @RequestParam("category") + final String category, + @EnumValue(enumClass = SortBy.class, message = "유효하지 않은 카테고리입니다", ignoreCase = false) + @RequestParam("sort") final String sort) { + final DiaryMyListRes diaryMyListRes = diaryService.getMyDiaryList(userId, category, sort); + return ResponseEntity.status(HttpStatus.OK).body(diaryMyListRes); + } + + //일기 상세 조회 API + @GetMapping("/diary/{diaryId}") + ResponseEntity getDiaryDetailInfo(@NotNull @Min(1) @RequestHeader("userId") final long userId, + @NotBlank @PathVariable final Long diaryId) { + final DiaryDetailInfoRes diaryDetailInfoRes = diaryService.getDiaryDetailInfo(userId, diaryId); + return ResponseEntity.status(HttpStatus.OK).body(diaryDetailInfoRes); + } + + //일기 수정 api + @PatchMapping("/diary/{diaryId}") + ResponseEntity editDiary(@NotNull @Min(1) @RequestHeader("userId") final long userId, + @NotBlank @PathVariable final Long diaryId, + @Valid @RequestBody final DiaryEditReq diaryEditReq) { + ValidatorUtil.validStringLength(diaryEditReq.content(), Constants.MAX_CONTENT_LENGTH); + diaryService.editDiary(userId, diaryId, diaryEditReq); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @DeleteMapping("diary/{diaryId}") + ResponseEntity deleteDiary(@NotNull @Min(1) @RequestHeader("userId") final long userId, + @NotBlank @PathVariable final Long diaryId) { + diaryService.deleteDiary(userId, diaryId); + return ResponseEntity.status(HttpStatus.OK).build(); + } +} diff --git a/src/main/java/org/sopt/diary/domain/diary/api/dto/req/DiaryEditReq.java b/src/main/java/org/sopt/diary/domain/diary/api/dto/req/DiaryEditReq.java new file mode 100644 index 0000000..40b7ac7 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/api/dto/req/DiaryEditReq.java @@ -0,0 +1,16 @@ +package org.sopt.diary.domain.diary.api.dto.req; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.sopt.diary.common.enums.Category; +import org.sopt.diary.common.enums.validation.EnumValue; + +public record DiaryEditReq( + @NotBlank(message = "contetn 값이 없으면 안됩니다.") + @Size(min = 1, max = 30, message = "일기내용은 1~30글자여야합니다.") + String content, + @EnumValue(enumClass = Category.class, message = "유효하지 않은 카테고리입니다", ignoreCase = false) + @NotBlank(message = "category 값이 없으면 안됩니다.") + Category category +) { +} diff --git a/src/main/java/org/sopt/diary/domain/diary/api/dto/req/DiaryPostReq.java b/src/main/java/org/sopt/diary/domain/diary/api/dto/req/DiaryPostReq.java new file mode 100644 index 0000000..e123f7e --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/api/dto/req/DiaryPostReq.java @@ -0,0 +1,26 @@ +package org.sopt.diary.domain.diary.api.dto.req; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.sopt.diary.common.enums.Category; +import org.sopt.diary.common.enums.validation.EnumValue; + +public record DiaryPostReq( + @NotBlank(message = "title 값이 없으면 안됩니다.") + @Size(min = 1, max = 10, message = "제목은 1~10글자여야됩니다.") + String title, + + @Size(min = 1, max = 30, message = "일기내용은 1~30글자여야합니다.") + @NotBlank(message = "content 값이 없으면 안됩니다.") + String content, + + @EnumValue(enumClass = Category.class, message = "유효하지 않은 카테고리입니다", ignoreCase = false) + @NotBlank(message = "카테고리 값이 없으면 안됩니다.") + String category, + + boolean isPrivate + ) { + +} diff --git a/src/main/java/org/sopt/diary/api/dto/res/DiaryDetailInfoRes.java b/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryDetailInfoRes.java similarity index 62% rename from src/main/java/org/sopt/diary/api/dto/res/DiaryDetailInfoRes.java rename to src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryDetailInfoRes.java index 5d6d036..a755b57 100644 --- a/src/main/java/org/sopt/diary/api/dto/res/DiaryDetailInfoRes.java +++ b/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryDetailInfoRes.java @@ -1,17 +1,19 @@ -package org.sopt.diary.api.dto.res; +package org.sopt.diary.domain.diary.api.dto.res; public record DiaryDetailInfoRes( Long id, String title, String content, - String createdDate + String createdDate, + String category ) { public static DiaryDetailInfoRes of( final Long id, final String title, final String content, - final String createdDate + final String createdDate, + final String category ) { - return new DiaryDetailInfoRes(id, title, content, createdDate); + return new DiaryDetailInfoRes(id, title, content, createdDate, category); } } diff --git a/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryListRes.java b/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryListRes.java new file mode 100644 index 0000000..2f96e9f --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryListRes.java @@ -0,0 +1,26 @@ +package org.sopt.diary.domain.diary.api.dto.res; + +import java.time.LocalDateTime; +import java.util.List; + +public record DiaryListRes( + List diaryList +) { + public static DiaryListRes of(final List diaryInfo) { + return new DiaryListRes(diaryInfo); + } + + public record DiaryInfo( + Long id, + String username, + String title, + LocalDateTime createAt + ) { + public static DiaryInfo of(final Long id, + final String username, + final String title, + final LocalDateTime createAt) { + return new DiaryInfo(id, username, title, createAt); + } + } +} diff --git a/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryMyListRes.java b/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryMyListRes.java new file mode 100644 index 0000000..59183c0 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/api/dto/res/DiaryMyListRes.java @@ -0,0 +1,24 @@ +package org.sopt.diary.domain.diary.api.dto.res; + +import java.time.LocalDateTime; +import java.util.List; + +public record DiaryMyListRes( + List diaryList +) { + public static DiaryMyListRes of(final List diaryInfo) { + return new DiaryMyListRes(diaryInfo); + } + + public record DiaryMyInfo( + Long id, + String title, + LocalDateTime createAt + ) { + public static DiaryMyListRes.DiaryMyInfo of(final Long id, + final String title, + final LocalDateTime createAt) { + return new DiaryMyListRes.DiaryMyInfo(id, title, createAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/diary/domain/diary/entity/DiaryEntity.java b/src/main/java/org/sopt/diary/domain/diary/entity/DiaryEntity.java new file mode 100644 index 0000000..a1815d5 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/entity/DiaryEntity.java @@ -0,0 +1,95 @@ +package org.sopt.diary.domain.diary.entity; + +import jakarta.persistence.*; +import org.sopt.diary.domain.users.entity.User; +import org.sopt.diary.common.enums.Category; +import org.sopt.diary.common.util.BaseTimeEntity; + +@Entity +@Table(name = "diary") +public class DiaryEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @Column(name = "title") + private String title; + + @Column(name = "content") + private String content; + + @Column(name = "is_private") + private Boolean isPrivate; + + @Column(name = "category") + @Enumerated(EnumType.STRING) + private Category category; + + public DiaryEntity() { } + + public static DiaryEntity create(final User user, + final String title, + final String content, + final Category category, + final boolean isPrivate) { + return new DiaryEntity(user, title, content, category, isPrivate); + } + + public DiaryEntity(User user, String title, String content, Category category, Boolean isPrivate) { + this.user = user; + this.title = title; + this.content = content; + this.isPrivate = isPrivate; + this.category = category; + } + + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public User getUser() { + return user; + } + + public void setPrivate(Boolean aPrivate) { + isPrivate = aPrivate; + } + + public Boolean getPrivate() { + return isPrivate; + } + + public void setId(final Long id) { + this.id = id; + } + + public void setTitle(final String title) { + this.title = title; + } + + public void setContent(final String content) { + this.content = content; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } +} diff --git a/src/main/java/org/sopt/diary/domain/diary/repository/DiaryRepository.java b/src/main/java/org/sopt/diary/domain/diary/repository/DiaryRepository.java new file mode 100644 index 0000000..59efb24 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/repository/DiaryRepository.java @@ -0,0 +1,52 @@ +package org.sopt.diary.domain.diary.repository; + +import org.sopt.diary.common.enums.Category; +import org.sopt.diary.domain.diary.entity.DiaryEntity; +import org.sopt.diary.domain.users.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public interface DiaryRepository extends JpaRepository { + + + // 최신순으로 10개 조회 (ALL 카테고리) - 공개된 일기만 + List findTop10ByIsPrivateFalseOrderByCreatedAtDesc(); + + // 글자 수 순으로 10개 조회 (ALL 카테고리) - 공개된 일기만 (네이티브 쿼리) + @Query(value = "SELECT * FROM diary d WHERE d.is_private = false ORDER BY LENGTH(d.content) DESC LIMIT 10", nativeQuery = true) + List findTop10ByIsPrivateFalseOrderByContentLengthAscNative(); + + // 카테고리가 있는 경우, 최신순으로 10개 조회 - 공개된 일기만 +// @Query("SELECT d FROM DiaryEntity d WHERE d.category = :category AND d.isPrivate = false ORDER BY d.createdAt DESC") + List findTop10ByCategoryAndIsPrivateFalseOrderByCreatedAtDesc(Category category); + + // 카테고리가 있는 경우, 글자 수 순으로 10개 조회 - 공개된 일기만 + @Query(value = "SELECT * FROM diary d WHERE d.category = :category AND d.is_private = false ORDER BY LENGTH(d.content) DESC LIMIT 10", nativeQuery = true) + List findTop10ByCategoryAndIsPrivateFalseOrderByContentLengthNative(@Param("category") Category category); + + // 사용자별 최신순으로 10개 조회 (ALL 카테고리) + List findTop10ByUserOrderByCreatedAtDesc(User user); + + // 사용자별 글자 수 순으로 10개 조회 (ALL 카테고리) + @Query(value = "SELECT * FROM diary d WHERE d.user_id = :userId ORDER BY LENGTH(d.content) DESC LIMIT 10", nativeQuery = true) + List findTop10ByUserOrderByContentLengthAscNative(@Param("userId") Long userId); + + + // 특정 카테고리의 사용자별 최신순으로 10개 조회 + List findTop10ByUserAndCategoryOrderByCreatedAtDesc(User user, Category category); + + // 특정 카테고리의 사용자별 글자 수 순으로 10개 조회 + @Query(value = "SELECT * FROM diary d WHERE d.user_id = :userId AND d.category = :category ORDER BY LENGTH(d.content) DESC LIMIT 10", nativeQuery = true) + List findTop10ByUserAndCategoryOrderByContentLengthNative(@Param("userId") Long userId, @Param("category") Category category); + + + +} + + + diff --git a/src/main/java/org/sopt/diary/domain/diary/service/DiaryService.java b/src/main/java/org/sopt/diary/domain/diary/service/DiaryService.java new file mode 100644 index 0000000..1c3f868 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/diary/service/DiaryService.java @@ -0,0 +1,175 @@ +package org.sopt.diary.domain.diary.service; + +import org.sopt.diary.common.enums.Category; +import org.sopt.diary.common.Failure.UserFailureInfo; +import org.sopt.diary.common.enums.SortBy; +import org.sopt.diary.common.util.ValidatorUtil; +import org.sopt.diary.domain.diary.api.dto.req.DiaryEditReq; +import org.sopt.diary.domain.diary.api.dto.res.DiaryDetailInfoRes; +import org.sopt.diary.domain.diary.api.dto.res.DiaryListRes; +import org.sopt.diary.common.Failure.DiaryFailureInfo; +import org.sopt.diary.common.util.DateFormatUtil; +import org.sopt.diary.domain.diary.api.dto.res.DiaryMyListRes; +import org.sopt.diary.domain.users.entity.User; +import org.sopt.diary.domain.users.repository.UserRepository; +import org.sopt.diary.exception.BusinessException; +import org.sopt.diary.domain.diary.entity.DiaryEntity; +import org.sopt.diary.domain.diary.repository.DiaryRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + + +@Service +@Transactional(readOnly = true) +public class DiaryService { + private final DiaryRepository diaryRepository; + private final UserRepository userRepository; + + public DiaryService(DiaryRepository diaryRepository, UserRepository userRepository) { + this.diaryRepository = diaryRepository; + this.userRepository = userRepository; + } + + //일기 작성 + @Transactional + public void createDiary(final long userId, + final String title, + final String content, + final String category, + final boolean isPrivate) { + final User foundUser = findUser(userId); + final DiaryEntity newDiaryEntity = DiaryEntity.create(foundUser, title, content, Category.valueOf(category), isPrivate); + diaryRepository.save(newDiaryEntity); + } + + // 전체 다이어리 목록 조회 + public DiaryListRes getDiaryList(final String category, final String sortBy) { + final Category categoryEnum = Category.valueOf(category.toUpperCase()); + final SortBy sortByEnum = SortBy.valueOf(sortBy.toUpperCase()); + + final List findDiaryEntityList; + + //카테고리 초기화상태 (카테고리 상관없이 전체 조회) + if (categoryEnum == Category.ALL) { + findDiaryEntityList = switch (sortByEnum) { + case LATEST -> diaryRepository.findTop10ByIsPrivateFalseOrderByCreatedAtDesc(); //최신순 정렬 + case QUANTITY -> diaryRepository.findTop10ByIsPrivateFalseOrderByContentLengthAscNative(); //글자수순 정렬 + }; + } else { //카테고리별로 조회 + findDiaryEntityList = switch (sortByEnum) { + case LATEST -> + diaryRepository.findTop10ByCategoryAndIsPrivateFalseOrderByCreatedAtDesc(categoryEnum); //최신순 정렬 + case QUANTITY -> + diaryRepository.findTop10ByCategoryAndIsPrivateFalseOrderByContentLengthNative(categoryEnum); //글자수순 정렬 + }; + } + + //빈 List 검증 + if (ValidatorUtil.isListEmpty(findDiaryEntityList)) { + throw new BusinessException(DiaryFailureInfo.DIARY_NOT_FOUND); + } + + // DiaryListRes 응답 생성 + List diaryInfoList = findDiaryEntityList.stream() + .map(diaryEntity -> DiaryListRes.DiaryInfo.of(diaryEntity.getId(), diaryEntity.getUser().getNickname(), diaryEntity.getTitle(), diaryEntity.getCreatedAt())) + .toList(); + return DiaryListRes.of(diaryInfoList); + } + + //내 일기 목록 조회 + public DiaryMyListRes getMyDiaryList(final long userId, + final String category, + final String sortBy) { + final User foundUser = findUser(userId); + final Category categoryEnum = Category.valueOf(category.toUpperCase()); + final SortBy sortByEnum = SortBy.valueOf(sortBy.toUpperCase()); + + final List findDiaryEntityList; + + if (categoryEnum == Category.ALL) { + // 카테고리가 ALL일 때 + findDiaryEntityList = switch (sortByEnum) { + case LATEST -> diaryRepository.findTop10ByUserOrderByCreatedAtDesc(foundUser); + case QUANTITY -> diaryRepository.findTop10ByUserOrderByContentLengthAscNative(userId); + }; + } else { + // 특정 카테고리일 때 + findDiaryEntityList = switch (sortByEnum) { + case LATEST -> diaryRepository.findTop10ByUserAndCategoryOrderByCreatedAtDesc(foundUser, categoryEnum); + case QUANTITY -> diaryRepository.findTop10ByUserAndCategoryOrderByContentLengthNative(userId, categoryEnum); + }; + } + + // 빈 리스트 검증 + if (ValidatorUtil.isListEmpty(findDiaryEntityList)) { + throw new BusinessException(DiaryFailureInfo.DIARY_NOT_FOUND); + } + + // DiaryMyListRes 응답 생성 + List diaryInfoList = findDiaryEntityList.stream() + .map(diaryEntity -> DiaryMyListRes.DiaryMyInfo.of(diaryEntity.getId(), diaryEntity.getTitle(), diaryEntity.getCreatedAt())) + .toList(); + return DiaryMyListRes.of(diaryInfoList); + } + + + //일기 상세 조회 + public DiaryDetailInfoRes getDiaryDetailInfo(final long userId, final Long diaryId) { + final User foundUser = findUser(userId); + final DiaryEntity findDiary = findDiary(diaryId); + + //일기의 주인이 맞는지 검증 + isDiaryByUser(foundUser, findDiary); + + final String createTimeString = DateFormatUtil.format(findDiary.getCreatedAt()); //LocalDateTime -> String + return DiaryDetailInfoRes.of(findDiary.getId(), findDiary.getTitle(), findDiary.getContent(), createTimeString, String.valueOf(findDiary.getCategory())); + } + + //일기 수정 + @Transactional + public void editDiary(final long userId, final Long diaryId, final DiaryEditReq diaryEditReq) { + final User foundUser = findUser(userId); + final DiaryEntity findDiary = findDiary(diaryId); + + //일기 작성자인지 검증 + isDiaryByUser(foundUser, findDiary); + + findDiary.setContent(diaryEditReq.content()); + findDiary.setCategory(diaryEditReq.category()); + } + + //일기 삭제 + @Transactional + public void deleteDiary(final long userId, final Long diaryId) { + final User foundUser = findUser(userId); + final DiaryEntity foundDiary = findDiary(diaryId); + + //일기 작성자 검증 + isDiaryByUser(foundUser, foundDiary); + + diaryRepository.deleteById(diaryId); + } + + //일기 찾기 + public DiaryEntity findDiary(final long diayId) { + return diaryRepository.findById(diayId).orElseThrow( + () -> new BusinessException(DiaryFailureInfo.DIARY_NOT_FOUND) + ); + } + + //유저 찾기 + public User findUser(final Long uuserId) { + return userRepository.findById(uuserId).orElseThrow( + () -> new BusinessException(UserFailureInfo.USER_NOT_FOUND) + ); + } + + //일기의 주인이 맞는지 검증 + private void isDiaryByUser(final User user, final DiaryEntity diary) { + if (!diary.getUser().equals(user)) { + throw new BusinessException(DiaryFailureInfo.UNAUTHORIZED_EXCEPTION); + } + } +} diff --git a/src/main/java/org/sopt/diary/domain/users/api/UserController.java b/src/main/java/org/sopt/diary/domain/users/api/UserController.java new file mode 100644 index 0000000..8284de7 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/api/UserController.java @@ -0,0 +1,34 @@ +package org.sopt.diary.domain.users.api; + +import org.sopt.diary.domain.users.api.dto.UserSignInRes; +import org.sopt.diary.domain.users.api.dto.UserSignUpReq; +import org.sopt.diary.domain.users.api.dto.UserSigninReq; +import org.sopt.diary.domain.users.service.UserServie; +import org.springframework.http.HttpStatus; +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.RestController; + +@RestController +public class UserController { + private final UserServie userServie; + + public UserController(UserServie userServie) { + this.userServie = userServie; + } + + @PostMapping("/users/signup") + public ResponseEntity signup(@RequestBody final UserSignUpReq userSignUpReq) { + userServie.signup(userSignUpReq.loginId(), userSignUpReq.password(), userSignUpReq.nickname()); + return ResponseEntity.ok().build(); + } + + @PostMapping("/users/signin") + public ResponseEntity signin(@RequestBody final UserSigninReq userSigninReq) { + final UserSignInRes userSignInRes = userServie.signin(userSigninReq.loginId(), userSigninReq.password()); + return ResponseEntity.status(HttpStatus.CREATED).body(userSignInRes); + } + + +} diff --git a/src/main/java/org/sopt/diary/domain/users/api/dto/UserSignInRes.java b/src/main/java/org/sopt/diary/domain/users/api/dto/UserSignInRes.java new file mode 100644 index 0000000..43b2372 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/api/dto/UserSignInRes.java @@ -0,0 +1,11 @@ +package org.sopt.diary.domain.users.api.dto; + +import org.sopt.diary.domain.users.entity.User; + +public record UserSignInRes( + Long userId +) { + public static UserSignInRes of(Long userId) { + return new UserSignInRes(userId); + } +} diff --git a/src/main/java/org/sopt/diary/domain/users/api/dto/UserSignUpReq.java b/src/main/java/org/sopt/diary/domain/users/api/dto/UserSignUpReq.java new file mode 100644 index 0000000..c4863dd --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/api/dto/UserSignUpReq.java @@ -0,0 +1,8 @@ +package org.sopt.diary.domain.users.api.dto; + +public record UserSignUpReq( + String loginId, + String password, + String nickname +) { +} diff --git a/src/main/java/org/sopt/diary/domain/users/api/dto/UserSigninReq.java b/src/main/java/org/sopt/diary/domain/users/api/dto/UserSigninReq.java new file mode 100644 index 0000000..7a89609 --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/api/dto/UserSigninReq.java @@ -0,0 +1,7 @@ +package org.sopt.diary.domain.users.api.dto; + +public record UserSigninReq( + String loginId, + String password +) { +} diff --git a/src/main/java/org/sopt/diary/domain/users/entity/User.java b/src/main/java/org/sopt/diary/domain/users/entity/User.java new file mode 100644 index 0000000..09fe76f --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/entity/User.java @@ -0,0 +1,44 @@ +package org.sopt.diary.domain.users.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "user") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "login_id") + private String loginId; + + @Column(name = "password") + private String password; + + @Column(name = "nickname") + private String nickname; + + public User() { } + + public static User create(final String loginId, final String password, final String nickname) { + return new User(loginId, password, nickname); + } + + public User(String loginId, String password, String nickname) { + this.loginId = loginId; + this.password = password; + this.nickname = nickname; + } + + public Long getId() { + return id; + } + + public String getPassword() { + return password; + } + + public String getNickname() { + return nickname; + } +} diff --git a/src/main/java/org/sopt/diary/domain/users/repository/UserRepository.java b/src/main/java/org/sopt/diary/domain/users/repository/UserRepository.java new file mode 100644 index 0000000..69783da --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/repository/UserRepository.java @@ -0,0 +1,11 @@ +package org.sopt.diary.domain.users.repository; + +import org.sopt.diary.domain.users.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByLoginId(String loginId); + boolean existsByLoginIdAndPassword(String loginId, String password); +} diff --git a/src/main/java/org/sopt/diary/domain/users/service/UserServie.java b/src/main/java/org/sopt/diary/domain/users/service/UserServie.java new file mode 100644 index 0000000..956f4da --- /dev/null +++ b/src/main/java/org/sopt/diary/domain/users/service/UserServie.java @@ -0,0 +1,43 @@ +package org.sopt.diary.domain.users.service; + +import ch.qos.logback.core.spi.ErrorCodes; +import jakarta.persistence.EntityNotFoundException; +import org.hibernate.annotations.NotFound; +import org.sopt.diary.common.Failure.CommonFailureInfo; +import org.sopt.diary.common.Failure.UserFailureInfo; +import org.sopt.diary.domain.users.api.dto.UserSignInRes; +import org.sopt.diary.domain.users.entity.User; +import org.sopt.diary.domain.users.repository.UserRepository; +import org.sopt.diary.exception.BusinessException; +import org.springframework.stereotype.Service; + + +@Service +public class UserServie { + private final UserRepository userRepository; + + public UserServie(UserRepository userRepository) { + this.userRepository = userRepository; + } + + //회원가입 + public void signup(final String loginId, final String password, final String nickName) { + + // todo: DB단에서 유니크키로 중복여부 검사(loginId & nickName) + userRepository.save(new User(loginId, password, nickName)); + } + + //로그인 + public UserSignInRes signin(final String loginId, final String password) { + // loginId로 유저찾기 + final User foundUser = userRepository.findByLoginId(loginId).orElseThrow( + () -> new BusinessException(UserFailureInfo.USER_NOT_FOUND) + ); + // 찾은 유저 비밀번호와 받은 비밀번호 비교 + if(foundUser.getPassword().equals(password)) { + return UserSignInRes.of(foundUser.getId()); + } else { + throw new BusinessException(UserFailureInfo.INVALID_USER_PASSWROD); + } + } +} diff --git a/src/main/java/org/sopt/diary/exception/BadRequestException.java b/src/main/java/org/sopt/diary/exception/BadRequestException.java deleted file mode 100644 index 971b4eb..0000000 --- a/src/main/java/org/sopt/diary/exception/BadRequestException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.sopt.diary.exception; - -import org.sopt.diary.common.Failure.FailureCode; - -public class BadRequestException extends BusinessException{ - public BadRequestException(final FailureCode failureCode) { - super(failureCode); - } -} diff --git a/src/main/java/org/sopt/diary/exception/NotFoundException.java b/src/main/java/org/sopt/diary/exception/NotFoundException.java deleted file mode 100644 index cf03ea5..0000000 --- a/src/main/java/org/sopt/diary/exception/NotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.sopt.diary.exception; - -import org.sopt.diary.common.Failure.FailureCode; - -public class NotFoundException extends BusinessException{ - public NotFoundException(final FailureCode failureCode) { - super(failureCode); - } -} diff --git a/src/main/java/org/sopt/diary/repository/DiaryEntity.java b/src/main/java/org/sopt/diary/repository/DiaryEntity.java deleted file mode 100644 index 7a6ebb6..0000000 --- a/src/main/java/org/sopt/diary/repository/DiaryEntity.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.sopt.diary.repository; - -import jakarta.persistence.*; -import org.sopt.diary.common.util.BaseTimeEntity; - -@Entity -public class DiaryEntity extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "title") - private String title; - - @Column(name = "content") - private String content; - - public DiaryEntity() { } - - public static DiaryEntity create(final String title, final String content) { - return new DiaryEntity(title, content); - } - - public DiaryEntity(final String title, final String content) { - this.title = title; - this.content = content; - } - - public void setId(final Long id) { - this.id = id; - } - - public void setTitle(final String title) { - this.title = title; - } - - public void setContent(final String content) { - this.content = content; - } - - public Long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public String getContent() { - return content; - } -} diff --git a/src/main/java/org/sopt/diary/repository/DiaryRepository.java b/src/main/java/org/sopt/diary/repository/DiaryRepository.java deleted file mode 100644 index 2256bed..0000000 --- a/src/main/java/org/sopt/diary/repository/DiaryRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.sopt.diary.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Optional; - -@Component -public interface DiaryRepository extends JpaRepository { - Optional> findTop10ByOrderByCreatedAtDesc(); -} diff --git a/src/main/java/org/sopt/diary/service/DiaryService.java b/src/main/java/org/sopt/diary/service/DiaryService.java deleted file mode 100644 index 2ad281e..0000000 --- a/src/main/java/org/sopt/diary/service/DiaryService.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.sopt.diary.service; - -import org.sopt.diary.api.dto.req.DiaryEditReq; -import org.sopt.diary.api.dto.req.DiaryPostReq; -import org.sopt.diary.api.dto.res.DiaryDetailInfoRes; -import org.sopt.diary.api.dto.res.DiaryListRes; -import org.sopt.diary.common.Failure.DiaryFailureInfo; -import org.sopt.diary.common.util.DateFormatUtil; -import org.sopt.diary.exception.NotFoundException; -import org.sopt.diary.repository.DiaryEntity; -import org.sopt.diary.repository.DiaryRepository; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Comparator; -import java.util.List; - - -@Component -@Transactional -public class DiaryService { - private final DiaryRepository diaryRepository; - - public DiaryService(final DiaryRepository diaryRepository) { - this.diaryRepository = diaryRepository; - } - - public void createDiary(final DiaryPostReq diaryPostReq) { - final DiaryEntity newDiaryEntity = DiaryEntity.create(diaryPostReq.title(), diaryPostReq.content()); - diaryRepository.save(newDiaryEntity); - } - - @Transactional(readOnly = true) - public DiaryListRes getDiaryList() { - final List findDiaryEntityList = diaryRepository.findTop10ByOrderByCreatedAtDesc().orElseThrow( - () -> new NotFoundException(DiaryFailureInfo.EMPTY_DIARY) - ); - if (findDiaryEntityList.isEmpty()) { - throw new NotFoundException(DiaryFailureInfo.EMPTY_DIARY); - } - List DiaryIdAndTitle = findDiaryEntityList.stream() - .sorted(Comparator.comparing(DiaryEntity::getId)) // ID를 오름차순으로 정렬 - .map(diaryEntity -> DiaryListRes.DiaryIdAndTitle.of(diaryEntity.getId(), diaryEntity.getTitle())) - .toList(); - - return DiaryListRes.of(DiaryIdAndTitle); - } - - @Transactional(readOnly = true) - public DiaryDetailInfoRes getDiaryDetailInfo(final Long id) { - final DiaryEntity findDiary = findDiary(id); - final String createTimeString = DateFormatUtil.format(findDiary.getCreatedAt()); //LocalDateTime -> String - - return DiaryDetailInfoRes.of(findDiary.getId(), findDiary.getTitle(), findDiary.getContent(), createTimeString); - } - - public void editDiaryContent(final Long id, final DiaryEditReq diaryEditReq) { - final DiaryEntity findDiary = findDiary(id); - findDiary.setContent(diaryEditReq.content()); //null 질문 답변 이후 처리 - } - - public void deleteDiary(final Long id) { - findDiary(id); - diaryRepository.deleteById(id); - } - - public DiaryEntity findDiary(final Long id) { - return diaryRepository.findById(id).orElseThrow( - () -> new NotFoundException(DiaryFailureInfo.DIARY_NOT_FOUND) - ); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 4c971d7..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,14 +0,0 @@ -spring: - datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:testdb - username: seongjoon - password: - jpa: - show-sql: true - hibernate: - ddl-auto: create - dialect: org.hibernate.dialect.H2Dialect - properties: - format_sql: true - show_sql: true \ No newline at end of file