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 extends Payload>[] payload() default { };
+
+ Class extends java.lang.Enum>> 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