diff --git a/BE/peaktime/.gitattributes b/BE/peaktime/.gitattributes deleted file mode 100644 index 604f01f6..00000000 --- a/BE/peaktime/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -/gradlew text eol=lf -*.bat text eol=crlf -*.jar binary -*.* filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/BE/peaktime/.gitignore b/BE/peaktime/.gitignore index 9edd96db..86762093 100644 --- a/BE/peaktime/.gitignore +++ b/BE/peaktime/.gitignore @@ -1,3 +1,41 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b90df4da0ef38547678dc6b8af7131cc00625bf9388ffb2a6d7d1aa69381bf8 -size 530 +HELP.md +.gradle +build/ +# QueryDSL Q 클래스 파일 무시 +src/main/generated/** +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ +### IntelliJ IDEA ### +.idea +.DS_Store +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +application-*.yml \ No newline at end of file diff --git a/BE/peaktime/build.gradle b/BE/peaktime/build.gradle index 8e164f34..71bf7e38 100644 --- a/BE/peaktime/build.gradle +++ b/BE/peaktime/build.gradle @@ -1,3 +1,102 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:987fff066d93ed7a02e8f1b4b28a48ab2b45ef0f893deef335721c7cf5d07aa7 -size 3436 +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.3' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + + // spring-boot-starter + implementation 'org.springframework.boot:spring-boot-starter' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-aop' + + // async retry dependency + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework:spring-aspects' + + // postgreSQL and Redis + implementation 'org.postgresql:postgresql:42.7.3' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // 레디스 직렬화 추가 + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + + // SMTP + implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Hibernate Types 라이브러리 추가 + implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3' + + // Lombok + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + + // Log4j + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.23.1' + + // querydsl 추가 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-oauth2-jose' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.6.0' + + + // JJWT (Java JSON Web Token) + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + + // Jakarta Validation API + implementation 'jakarta.validation:jakarta.validation-api:3.0.2' + + // Jackson module (for using 'LocalDateTime') + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.3' + + // Hibernate Validator (Reference Implementation) + implementation 'org.hibernate.validator:hibernate-validator:7.0.4.Final' + + // Test dependencies + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'javax.servlet:javax.servlet-api:4.0.1' + + // Lombok in tests + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/BE/peaktime/gradle/wrapper/.gitattributes b/BE/peaktime/gradle/wrapper/.gitattributes deleted file mode 100644 index 661352eb..00000000 --- a/BE/peaktime/gradle/wrapper/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.* filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/BE/peaktime/gradle/wrapper/gradle-wrapper.jar b/BE/peaktime/gradle/wrapper/gradle-wrapper.jar index cdd14641..a4b76b95 100644 Binary files a/BE/peaktime/gradle/wrapper/gradle-wrapper.jar and b/BE/peaktime/gradle/wrapper/gradle-wrapper.jar differ diff --git a/BE/peaktime/gradlew.bat b/BE/peaktime/gradlew.bat index bcc1fd09..9d21a218 100644 --- a/BE/peaktime/gradlew.bat +++ b/BE/peaktime/gradlew.bat @@ -1,3 +1,94 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2209f919a22528af59a2af2ad97e8d056cca18e39f7d87aa3fd549a73b180150 -size 2872 +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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=. +@rem This is normally unused +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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/controller/ChildController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/controller/ChildController.java index c668271f..169a3446 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/controller/ChildController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/controller/ChildController.java @@ -1,3 +1,129 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70d469693c11e04eb46a9d7feac5928623be61893507de2fbe30d691682e44f0 -size 7536 +package com.dinnertime.peaktime.domain.child.controller; + +import com.dinnertime.peaktime.domain.child.service.ChildService; +import com.dinnertime.peaktime.domain.child.service.dto.request.ChangeChildPasswordRequestDto; +import com.dinnertime.peaktime.domain.child.service.dto.request.CreateChildRequestDto; +import com.dinnertime.peaktime.domain.child.service.dto.request.UpdateChildRequestDto; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.core.Filter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/children") +public class ChildController { + + private final ChildService childService; + + @Operation(summary = "자식 계정 생성", description = "루트 유저가 자식 계정을 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "자식 계정 생성에 성공하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "404", description = "존재하지 않는 계정입니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "422", description = "그룹에는 최대 30명의 자식 계정만 존재할 수 있습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "500", description = "자식 계정 생성에 실패하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("") + public ResponseEntity createChild(@Valid @RequestBody CreateChildRequestDto requestDto){ + + childService.createChild(requestDto); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), "자식 계정 생성에 성공하였습니다.")); + } + + @Operation(summary = "자식 계정 삭제", description = "루트 유저가 자식 계정을 삭제") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "자식 계정 삭제에 성공하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "404", description = "존재하지 않는 계정입니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "500", description = "자식 계정 삭제에 실패하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}) + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("/{child-id}") + public ResponseEntity deleteChild(@PathVariable("child-id") Long childId){ + + childService.deleteChild(childId); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), "자식 계정 삭제에 성공하였습니다.")); + } + + @Operation(summary = "자식 계정 수정", description = "루트 유저가 자식 계정을 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "자식 계정 수정에 성공하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "404", description = "존재하지 않는 계정입니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "500", description = "자식 계정 수정에 실패하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/{child-id}") + public ResponseEntity updateChild(@PathVariable("child-id") Long childId, + @Valid @RequestBody UpdateChildRequestDto requestDto){ + + childService.updateChild(childId, requestDto); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), "자식 계정 수정에 성공하였습니다.")); + } + + @Operation(summary = "자식 계정 비밀번호 변경", description = "루트 유저가 자식 계정의 비밀번호 변경") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "자식 계정 비밀번호 변경에 성공하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "404", description = "존재하지 않는 계정입니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "500", description = "자식 계정 비밀번호 변경에 실패하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/{child-id}/password") + public ResponseEntity changeChildPassword(@PathVariable("child-id") Long childId, + @Valid @RequestBody ChangeChildPasswordRequestDto requestDto){ + + childService.changeChildPassword(childId, requestDto); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), "자식 계정 수정에 성공하였습니다.")); + } + + @Operation(summary = "자식 계정 비밀번호 초기화", description = "루트 유저가 자식 계정의 비밀번호 초기화") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "자식 계정 비밀번호 초기화에 성공하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "404", description = "존재하지 않는 계정입니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}), + @ApiResponse(responseCode = "500", description = "자식 계정 비밀번호 초기화에 실패하였습니다.", + content = {@Content(schema = @Schema(implementation = ResultDto.class))}) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/{child-id}/init-password") + public ResponseEntity initChildPassword(@PathVariable("child-id") Long childId){ + + childService.initChildPassword(childId); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), "자식 계정 비밀번호 초기화에 성공하였습니다.")); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/ChildService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/ChildService.java index a07fc205..1ad40781 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/ChildService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/ChildService.java @@ -1,3 +1,190 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73b9c137a3708dd9be0c43356b9b8d5edbdfab9b342bacae07357fbd2f5f5cf6 -size 7840 +package com.dinnertime.peaktime.domain.child.service; + +import com.dinnertime.peaktime.domain.child.service.dto.request.ChangeChildPasswordRequestDto; +import com.dinnertime.peaktime.domain.child.service.dto.request.CreateChildRequestDto; +import com.dinnertime.peaktime.domain.child.service.dto.request.UpdateChildRequestDto; +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import com.dinnertime.peaktime.domain.statistic.repository.StatisticRepository; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.domain.user.service.UserService; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.auth.service.AuthService; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.AuthUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChildService { + + private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; + private final UserGroupRepository userGroupRepository; + private final GroupRepository groupRepository; + private final StatisticRepository statisticRepository; + + // 초기화 비밀번호 설정 + private static final String initPassword = "000000"; + + @Transactional + public void createChild(CreateChildRequestDto requestDto){ + + // 1. 해당 그룹의 인원이 30명 미만인지 확인 + Group group = groupRepository.findByGroupIdAndIsDeleteFalse(requestDto.getGroupId()) + .orElseThrow(() -> new CustomException(ErrorCode.GROUP_NOT_FOUND)); + + Long userCount = userGroupRepository.countAllByGroup_groupId(requestDto.getGroupId()); + if(userCount >= 30) { + throw new CustomException(ErrorCode.FAILED_CREATE_CHILD_USER); + } + + // 2. 아이디 형식 확인 + if(!AuthUtil.checkFormatValidationUserLoginId(requestDto.getChildLoginId())){ + throw new CustomException(ErrorCode.INVALID_USER_LOGIN_ID_FORMAT); + } + + // 3. 아이디 소문자 변환 + String childLoginId = AuthUtil.convertUpperToLower(requestDto.getChildLoginId()); + + // 4. 아이디 중복 확인 + if(this.checkDuplicateUserLoginId(childLoginId)){ + throw new CustomException(ErrorCode.DUPLICATED_USER_LOGIN_ID); + }; + + // 5. 닉네임 형식 확인 + if(!AuthUtil.checkFormatValidationNickname(requestDto.getChildNickname())){ + throw new CustomException(ErrorCode.INVALID_NICKNAME_FORMAT); + } + + // 6. 비밀번호 암호화 + String encodedPassword = passwordEncoder.encode(initPassword); + + // 7. 유저 테이블 저장 + User user = User.createChildUser(requestDto.getChildLoginId(), encodedPassword, requestDto.getChildNickname()); + userRepository.save(user); + + //8. 통계 테이블 저장 + Statistic statistic = Statistic.createFirstStatistic(user); + statisticRepository.save(statistic); + + // 9. 유저 그룹 테이블 저장 + UserGroup userGroup = UserGroup.createUserGroup(user, group); + userGroupRepository.save(userGroup); + } + + @Transactional + public void deleteChild(Long childId){ + // 1. 자식 계정 확인 + User childUser = this.getChildUser(childId); + + // 2. user_group 테이블 삭제 + UserGroup userGroup = this.getChildUserGroup(childId); + + userGroupRepository.delete(userGroup); + + // 3. user 테이블 수정 및 저장 + childUser.deleteUser(); + userRepository.save(childUser); + } + + @Transactional + public void updateChild(Long childId, UpdateChildRequestDto requestDto){ + + // 1. 유저 테이블 조회 + User childUser = this.getChildUser(childId); + + // 2. 유저그룹 테이블 조회 + UserGroup userGroup = this.getChildUserGroup(childId); + + // 3. 그룹이 변경될 경우 + if(!userGroup.getGroup().getGroupId().equals(requestDto.getGroupId())){ + // 4. 그룹 조회 + Group group = groupRepository.findByGroupIdAndIsDeleteFalse(requestDto.getGroupId()) + .orElseThrow(() -> new CustomException(ErrorCode.GROUP_NOT_FOUND)); + + // 5. 해당 그룹의 인원이 30명 미만인지 확인 + Long userCount = userGroupRepository.countAllByGroup_groupId(requestDto.getGroupId()); + if(userCount >= 30) { + throw new CustomException(ErrorCode.FAILED_CREATE_CHILD_USER); + } + + // 6. 유저그룹 수정 및 저장 + userGroup.changeUserGroup(group); + userGroupRepository.save(userGroup); + } + + // 7. 유저 닉네임이 변경될 경우 + if(!childUser.getNickname().equals(requestDto.getChildNickName())){ + // 8. 변경 닉네임이 형식에 맞는지 확인 + if(!AuthUtil.checkFormatValidationNickname(requestDto.getChildNickName())){ + throw new CustomException(ErrorCode.INVALID_NICKNAME_FORMAT); + } + + // 9. 유저 수정 후 저장 + childUser.updateNickname(requestDto.getChildNickName()); + userRepository.save(childUser); + } + } + + @Transactional + public void changeChildPassword(Long childId, ChangeChildPasswordRequestDto requestDto){ + // 1. 자식 계정 조회 + User childUser = this.getChildUser(childId); + + // 2. 패스워드 형식 확인 + if(!AuthUtil.checkFormatValidationPassword(requestDto.getChildPassword())){ + throw new CustomException(ErrorCode.INVALID_PASSWORD_FORMAT); + } + + // 3. 패스워드 일치 확인 + if(!requestDto.getChildPassword().equals(requestDto.getChildConfirmPassword())){ + throw new CustomException(ErrorCode.NOT_EQUAL_PASSWORD); + } + // 4. 패스워드 암호화 + String encodedPassword = passwordEncoder.encode(requestDto.getChildPassword()); + + // 5. 수정 후 저장 + childUser.updatePassword(encodedPassword); + userRepository.save(childUser); + } + + @Transactional + public void initChildPassword(Long childId){ + // 1. 자식 계정 조회 + User childUser = this.getChildUser(childId); + + // 2. 초기화 비밀번호 설정 + String encodedPassword = passwordEncoder.encode(initPassword); + + // 3. 저장 + childUser.updatePassword(encodedPassword); + userRepository.save(childUser); + } + + // 아이디 중복 검사 (유저 로그인 아이디로 검사. 이미 존재하면 true 반환) + private boolean checkDuplicateUserLoginId(String userLoginId) { + return userRepository.findByUserLoginId(userLoginId).isPresent(); + } + + // 자식 계정 조회 + private User getChildUser(Long childId){ + return userRepository.findByUserIdAndIsDeleteFalseAndIsRootFalse(childId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } + + // 유저 그룹 테이블 조회 + private UserGroup getChildUserGroup(Long childId){ + return userGroupRepository.findByUser_UserId(childId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/ChangeChildPasswordRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/ChangeChildPasswordRequestDto.java index 5e4652e2..0ba17f80 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/ChangeChildPasswordRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/ChangeChildPasswordRequestDto.java @@ -1,3 +1,20 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:511b793a04e37215dd31a871b5976a7989e5c55a365616ba11095326d27e8eed -size 644 +package com.dinnertime.peaktime.domain.child.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChangeChildPasswordRequestDto { + + @NotBlank + @Length(min = 8, message = "최소 8자 이상의 패스워드를 입력해주세요.") + private String childPassword; + + @NotBlank + @Length(min = 8, message = "최소 8자 이상의 패스워드를 입력해주세요.") + private String childConfirmPassword; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/CreateChildRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/CreateChildRequestDto.java index 2907b5d0..9169e55d 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/CreateChildRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/CreateChildRequestDto.java @@ -1,3 +1,26 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba3c02db88810ba5e0b2c3cb5b3d4e947b16cfa3d3e03f7951dd1e4ea848d99c -size 772 +package com.dinnertime.peaktime.domain.child.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.hibernate.validator.constraints.Length; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +public class CreateChildRequestDto { + + @NotNull + private Long groupId; + + @NotBlank + @Length(min = 5, max = 15, message = "5자 이상 15자 이하의 아이디를 입력해주세요.") + private String childLoginId; + + @NotBlank + @Length(min = 2, max = 15, message = "2자 이상 15자 이하의 닉네임을 입력주세요.") + private String childNickname; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/UpdateChildRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/UpdateChildRequestDto.java index 44201a28..85249625 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/UpdateChildRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/child/service/dto/request/UpdateChildRequestDto.java @@ -1,3 +1,19 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:30b16c28ae044f20b3f59c21178c5d69a399731fc0f6c20c8fb60fac0313eea3 -size 568 +package com.dinnertime.peaktime.domain.child.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class UpdateChildRequestDto { + + @NotNull + Long groupId; + @NotBlank + @Length(min = 2, max = 15, message = "2자 이상 15자 이하의 닉네임을 입력주세요.") + String childNickName; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/entity/Content.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/entity/Content.java index b5f8871a..93339b04 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/entity/Content.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/entity/Content.java @@ -1,3 +1,57 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11168ca887984e5ec6ac111c7639e23b5514f6476fc94e70e1be03c3700e41c6 -size 1706 +package com.dinnertime.peaktime.domain.content.entity; + +import com.dinnertime.peaktime.domain.hiking.entity.Hiking; +import com.dinnertime.peaktime.domain.hiking.service.dto.request.ContentListRequestDto; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "contents") +public class Content { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "content_id") + private Long contentId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "hiking_id", nullable = false) + private Hiking hiking; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "type", nullable = false, length = 10) + private String type; + + @Column(name = "using_time", nullable = false) + private Integer usingTime; + + @Column(name = "is_blocked", nullable = false) + private Boolean isBlocked; + + @Builder + private Content(Hiking hiking, String name, String type, Integer usingTime, Boolean isBlocked) { + this.hiking = hiking; + this.name = name; + this.type = type; + this.usingTime = usingTime; + this.isBlocked = isBlocked; + } + + + public static Content createContent(Hiking hiking, ContentListRequestDto requestDto) { + return Content.builder() + .hiking(hiking) + .name(requestDto.getContentName()) + .type(requestDto.getContentType()) + .usingTime(requestDto.getUsingTime()) + .isBlocked(requestDto.getIsBlockContent()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/repository/ContentRepositoryImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/repository/ContentRepositoryImpl.java index 67a17165..2c16bdca 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/repository/ContentRepositoryImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/content/repository/ContentRepositoryImpl.java @@ -1,3 +1,59 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2b183faa03279f236a8cb6f0dd99a31f17d7bc3058e42fe8ede17ab5e799c33a -size 2316 +package com.dinnertime.peaktime.domain.content.repository; + +import com.dinnertime.peaktime.domain.content.entity.QContent; +import com.dinnertime.peaktime.domain.statistic.entity.StatisticContent; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ContentRepositoryImpl implements ContentRepositoryCustom { + + private final QContent content = QContent.content; + private final JPAQueryFactory queryFactory; + + // "site"와 "program" 타입의 상위 5개 BlockInfo 리스트 가져오는 메서드 + @Override + public List getTopUsingInfoList(String type, Long hikingId) { + return queryFactory.select(Projections.fields( + StatisticContent.class, + content.usingTime.sum().as("usingTime"), + content.name.as("name") + )) + .from(content) + .where( + content.hiking.hikingId.eq(hikingId) + .and(content.type.eq(type)) + .and(content.isBlocked.isFalse()) + .and(content.hiking.realEndTime.isNotNull()) + ) + .groupBy(content.name) + .orderBy(content.usingTime.sum().desc()) + .limit(5) + .fetch(); + } + + @Override + public List getTopUsingInfoListByUserId(String type, Long userId) { + return queryFactory.select(Projections.fields( + StatisticContent.class, + content.usingTime.sum().as("usingTime"), + content.name.as("name") + )) + .from(content) + .where(content.hiking.user.userId.eq(userId) + .and(content.type.eq(type)) + .and(content.isBlocked.isFalse()) + .and(content.hiking.realEndTime.isNotNull()) + ) + .groupBy(content.name) + .orderBy(content.usingTime.sum().desc()) + .limit(5) + .fetch(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/controller/GroupController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/controller/GroupController.java index 72ade655..946e6985 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/controller/GroupController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/controller/GroupController.java @@ -1,3 +1,131 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f1413ac7757847dd3cbeda4f1f99cffbda44971329be4535075c696a32cbc72 -size 8350 +package com.dinnertime.peaktime.domain.group.controller; + +import com.dinnertime.peaktime.domain.group.service.GroupService; +import com.dinnertime.peaktime.domain.group.service.dto.request.GroupCreateRequestDto; +import com.dinnertime.peaktime.domain.group.service.dto.request.GroupPutRequestDto; +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupDetailResponseDto; +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupListResponseDto; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.apache.coyote.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/groups") +@RequiredArgsConstructor +public class GroupController { + + private final GroupService groupService; + +// 그룹 전체 조회 + @Operation(summary = "그룹 전체 정보 조회", description = "루트 유저의 전체 그룹 정보 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "그룹 및 서브유저 전체 조회 성공하였습니다.", + content = @Content(schema = @Schema(implementation = GroupListResponseDto.class))), + @ApiResponse(responseCode = "500", description = "그룹을 조회하는 데 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("") + public ResponseEntity getGroupList(@AuthenticationPrincipal UserPrincipal userPrincipal) { + Long userId = userPrincipal.getUserId(); + + GroupListResponseDto groupListResponseDto = groupService.getGroupListResponseDto(userId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "그룹 및 서브유저 전체 조회 성공했습니다.", groupListResponseDto)); + } + +// 그룹 생성 + @Operation(summary = "그룹 생성", description = "유저가 가진 그룹 수와 그룹명 중복을 체크 후 그룹 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "그룹을 생성하는 데 성공했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "중복된 그룹 이름입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "422", description = "최대 5개까지 그룹을 생성할 수 있습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "그룹을 생성하는 데 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("") + public ResponseEntity postGroup(@AuthenticationPrincipal UserPrincipal userPrincipal, @RequestBody @Valid GroupCreateRequestDto requestDto) { + Long userId = userPrincipal.getUserId(); + + groupService.postGroup(userId, requestDto); + GroupListResponseDto groupListResponseDto = groupService.getGroupListResponseDto(userId); + return ResponseEntity.status(HttpStatus.CREATED).body(ResultDto.res(HttpStatus.CREATED.value(), "그룹을 생성하는 데 성공했습니다.", groupListResponseDto)); + } + +// 그룹 조회 + @Operation(summary = "그룹 단일 조회", description = "루트 유저의 그룹 단일 상세 정보 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "그룹을 조회하는 데 성공했습니다.", + content = @Content(schema = @Schema(implementation = GroupDetailResponseDto.class))), + @ApiResponse(responseCode = "422", description = "존재하지 않는 그룹입니다", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "그룹을 조회하는 데 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/{groupId}") + public ResponseEntity getGroupDetail(@PathVariable("groupId") Long groupId) { + GroupDetailResponseDto groupDetailResponseDto = groupService.getGroupDetail(groupId); + + return ResponseEntity.status(HttpStatus.OK).body((ResultDto.res(HttpStatus.OK.value(), "그룹을 조회하는 데 성공했습니다.", groupDetailResponseDto))); + } + +// 그룹 수정 + @Operation(summary = "그룹 수정", description = "그룹의 title 혹은 preset 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "그룹 정보를 수정하는 데 성공했습니다.", + content = @Content(schema = @Schema(implementation = GroupListResponseDto.class))), + @ApiResponse(responseCode = "409", description = "중복된 그룹 이름입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "422", description = "존재하지 않는 그룹입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "그룹 정보를 수정하는 데 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/{groupId}") + public ResponseEntity putGroup(@AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable("groupId") Long groupId, @RequestBody @Valid GroupPutRequestDto requestDto) { + Long userId = userPrincipal.getUserId(); + + groupService.putGroup(userId, groupId, requestDto); + GroupListResponseDto groupListResponseDto = groupService.getGroupListResponseDto(userId); + + return ResponseEntity.status(HttpStatus.OK).body((ResultDto.res(HttpStatus.OK.value(), "그룹 정보를 수정하는 데 성공했습니다.", groupListResponseDto))); + } + +// 그룹 삭제 + @Operation(summary = "그룹 삭제", description = "그룹 및 그룹에 속한 모든 child 계정 삭제") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "그룹 정보를 삭제하는 데 성공했습니다.", + content = @Content(schema = @Schema(implementation = GroupListResponseDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 그룹입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "그룹을 삭제하는 데 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("/{groupId}") + public ResponseEntity deleteGroup(@AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable("groupId") Long groupId) { + Long userId = userPrincipal.getUserId(); + groupService.deleteGroup(groupId); + GroupListResponseDto groupListResponseDto = groupService.getGroupListResponseDto(userId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "그룹 정보를 삭제하는 데 성공했습니다.", groupListResponseDto)); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/entity/Group.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/entity/Group.java index 17d3114e..1cf99e85 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/entity/Group.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/entity/Group.java @@ -1,3 +1,60 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5414b7ae971e15f75eb8445af3e8abc0b0df89caf6403d55ea8089dcae0b65d5 -size 1590 +package com.dinnertime.peaktime.domain.group.entity; + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import com.dinnertime.peaktime.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "groups") +public class Group { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "group_id") + private Long groupId; + + @Column(name = "title", length = 32, nullable = false) + private String title; + + @Column(name = "is_delete", nullable = false) + private Boolean isDelete = false; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "preset_id", nullable = false) + private Preset preset; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "root_user_id", nullable = false) + private User user; + + @Builder + private Group(String title, Preset preset, User user) { + this.title = title; + this.isDelete = false; + this.preset = preset; + this.user = user; + } + + public static Group createGroup(String title, Preset preset, User user) { + return Group.builder() + .title(title) + .preset(preset) + .user(user) + .build(); + } + + public void updateGroup(String newTitle, Preset newPreset) { + this.title = newTitle; + this.preset = newPreset; + } + + public void deleteGroup() { + this.isDelete = true; + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepository.java index 42fa7af5..13df19f1 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepository.java @@ -1,3 +1,29 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efecd8a2d0bc0538acc0fbeb91bf10e1f6457fc89b4d7dc4f3a285d39f53e9fb -size 1024 +package com.dinnertime.peaktime.domain.group.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import com.dinnertime.peaktime.domain.group.entity.Group; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface GroupRepository extends JpaRepository, GroupRepositoryCustom { + + // 그룹 전체 조회 + // 삭제되지 않은 group만 전체 조회 + List findByUser_UserIdAndIsDeleteFalseOrderByTitleAsc(Long userId); + + // 그룹 조회 + // groupId로 group 조회, 삭제되지 않은 그룹만 조회 + Optional findByGroupIdAndIsDeleteFalse(Long groupId); + + // 그룹 생성 전 그룹 수 조회 + List findByUser_UserIdAndIsDeleteFalse(Long userId); + + // 그룹 수정 전 그룹명 중복검사 + Long countByUser_UserIdAndIsDeleteFalseAndTitleAndGroupIdNot(Long userId, String title, Long groupId); + + Optional findByGroupId(Long groupId); + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepositoryCustomImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepositoryCustomImpl.java index 0833d019..ba97d8a2 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepositoryCustomImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/repository/GroupRepositoryCustomImpl.java @@ -1,3 +1,28 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:89bffb979c284a91e4406f2d0c63b2bfecc84ad82807b30b372507fb6dc5e783 -size 1016 +package com.dinnertime.peaktime.domain.group.repository; + +import com.dinnertime.peaktime.domain.user.entity.QUser; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.domain.usergroup.entity.QUserGroup; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class GroupRepositoryCustomImpl implements GroupRepositoryCustom { + + private final JPAQueryFactory queryFactory; + private final QUserGroup userGroup = QUserGroup.userGroup; + private final QUser user = QUser.user; + + @Override + public List findUserListByGroupId(Long groupId) { + return queryFactory.select(user) + .from(userGroup) + .where(userGroup.group.groupId.eq(groupId).and(userGroup.user.isDelete.isFalse())) + .fetch(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/GroupService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/GroupService.java index 7d4f4636..11b14138 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/GroupService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/GroupService.java @@ -1,3 +1,160 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:385b33c298549ec8066bed091b1acfd2315cfbd612298f5d14fd5467de049758 -size 6877 +package com.dinnertime.peaktime.domain.group.service; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.group.service.dto.request.GroupCreateRequestDto; +import com.dinnertime.peaktime.domain.group.service.dto.request.GroupPutRequestDto; +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupDetailResponseDto; +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupItemResponseDto; +import com.dinnertime.peaktime.domain.group.service.dto.response.ChildItemResponseDto; +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupListResponseDto; +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import com.dinnertime.peaktime.domain.preset.repository.PresetRepository; +import com.dinnertime.peaktime.domain.timer.repository.TimerRepository; +import com.dinnertime.peaktime.domain.timer.service.dto.response.TimerItemResponseDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GroupService { + + private final GroupRepository groupRepository; + private final UserGroupRepository userGroupRepository; + private final TimerRepository timerRepository; + private final UserRepository userRepository; + private final PresetRepository presetRepository; + + @Transactional + public GroupListResponseDto getGroupListResponseDto(Long userId) { + List groupList = getGroupList(userId); + + return GroupListResponseDto.createGroupListResponseDto(groupList); + } + + @Transactional + public List getGroupList(Long userId) { + List groupList = groupRepository.findByUser_UserIdAndIsDeleteFalseOrderByTitleAsc(userId); + + return groupList.stream() + .map(groupItem -> GroupItemResponseDto.createGroupItemResponseDto( + groupItem.getGroupId(), + groupItem.getTitle(), + getChildList(groupItem) + )) + .collect(Collectors.toList()); + } + + @Transactional + public List getChildList(Group group) { + List userGroups = userGroupRepository.findAllByGroup(group); + + return userGroups.stream() + .sorted(Comparator.comparing(userGroup -> userGroup.getUser().getNickname())) // nickname 오름차순 정렬 + .map(userGroup -> { + User user = userGroup.getUser(); + + return ChildItemResponseDto.createChildItemResponseDto( + user.getUserId(), + user.getUserLoginId(), + user.getNickname() + ); + }) + .collect(Collectors.toList()); + } + + + // 개별 그룹 조회 + @Transactional + public GroupDetailResponseDto getGroupDetail(Long groupId) { + // 그룹 조회 + Group group = groupRepository.findByGroupIdAndIsDeleteFalse(groupId) + .orElseThrow(() -> new CustomException(ErrorCode.GROUP_NOT_FOUND)); + + // 타이머 리스트 조회 + List timerList = timerRepository.findByGroup_GroupId(groupId) + .stream() + .map(TimerItemResponseDto::createTimeItemResponseDto) + .collect(Collectors.toList()); + + return GroupDetailResponseDto.createGroupDetailResponseDto(group, timerList); + } + + // 그룹 생성 + @Transactional + public void postGroup(Long userId, GroupCreateRequestDto requestDto) { + User user = userRepository.findByUserIdAndIsDeleteFalse(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + List groupListByUserId = groupRepository.findByUser_UserIdAndIsDeleteFalse(userId); + + // 그룹 수 확인 + if (groupListByUserId.size() >= 5) { + throw new CustomException(ErrorCode.FAILED_CREATE_GROUP); + } + + // 그룹명 중복 확인 + if (groupListByUserId.stream().anyMatch(group -> group.getTitle().equals(requestDto.getTitle()))) { + throw new CustomException(ErrorCode.GROUP_NAME_ALREADY_EXISTS); + } + + Preset preset = presetRepository.findByPresetId(requestDto.getPresetId()) + .orElseThrow(() -> new CustomException(ErrorCode.PRESET_NOT_FOUND)); + + // 생성 + Group group = Group.createGroup(requestDto.getTitle(), preset, user); + + groupRepository.save(group); + } + + // 그룹 수정 + @Transactional + public void putGroup(Long userId, Long groupId, GroupPutRequestDto requestDto) { + + // 그룹 조회 + Group groupSelected = groupRepository.findByGroupIdAndIsDeleteFalse(groupId) + .orElseThrow(() -> new CustomException(ErrorCode.GROUP_NOT_FOUND)); + + // 그룹명 중복 검사 + Long countTitle = groupRepository.countByUser_UserIdAndIsDeleteFalseAndTitleAndGroupIdNot(userId, requestDto.getTitle(), groupId); + if (countTitle > 0) { + throw new CustomException(ErrorCode.GROUP_NAME_ALREADY_EXISTS); + } + + // 그룹 정보 업데이트 + // 아무것도 바뀌지 않았다면 return + if (groupSelected.getTitle().equals(requestDto.getTitle()) && groupSelected.getPreset().getPresetId().equals(requestDto.getPresetId())) return; + + // title, preset 중 하나 이상 바뀌었다면 실행 + Preset preset = presetRepository.findByPresetId(requestDto.getPresetId()) + .orElseThrow(() -> new CustomException(ErrorCode.PRESET_NOT_FOUND)); + + groupSelected.updateGroup(requestDto.getTitle(), preset); + groupRepository.save(groupSelected); + } + + // 그룹 삭제 + @Transactional + public void deleteGroup(Long groupId) { + Group groupSelected = groupRepository.findByGroupIdAndIsDeleteFalse(groupId) + .orElseThrow(() -> new CustomException(ErrorCode.GROUP_NOT_FOUND)); + + // 그룹에 속해있는 child_user를 검색해서 삭제하기 + userRepository.updateIsDeleteByGroupId(groupId); + + groupSelected.deleteGroup(); + groupRepository.save(groupSelected); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupCreateRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupCreateRequestDto.java index 43d76f4b..331d4e52 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupCreateRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupCreateRequestDto.java @@ -1,3 +1,20 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1652644ef8b4107f0c53be152e604dd558a298455128d0a11be9d77bba5b366a -size 568 +package com.dinnertime.peaktime.domain.group.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GroupCreateRequestDto { + + @NotBlank + @Length(max = 32, message = "그룹명은 최대 32자를 초과할 수 없습니다.") + private String title; + + @NotNull + private Long presetId; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupPutRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupPutRequestDto.java index 35a4f2b2..0a22941c 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupPutRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/request/GroupPutRequestDto.java @@ -1,3 +1,21 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e72852b044655950b058f0b86ff1dd4988b8942b826215bf4569ee531d292101 -size 624 +package com.dinnertime.peaktime.domain.group.service.dto.request; + +import com.dinnertime.peaktime.global.exception.ErrorCode; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GroupPutRequestDto { + + @NotBlank + @Length(max = 32, message = "그룹명은 최대 32자를 초과할 수 없습니다.") + private String title; + + @NotNull + private Long presetId; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/ChildItemResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/ChildItemResponseDto.java index 2c58a75c..3f38d0c5 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/ChildItemResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/ChildItemResponseDto.java @@ -1,3 +1,29 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95d140eba4c4272ad23fd0b6e78197933b0883c22c39338c94299fb685b0c653 -size 873 +package com.dinnertime.peaktime.domain.group.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChildItemResponseDto { + private Long userId; + private String userLoginId; + private String nickname; + + @Builder + private ChildItemResponseDto(Long userId, String userLoginId, String nickname) { + this.userId = userId; + this.userLoginId = userLoginId; + this.nickname = nickname; + } + + public static ChildItemResponseDto createChildItemResponseDto(Long userId, String userLoginId, String nickname) { + return ChildItemResponseDto.builder() + .userId(userId) + .userLoginId(userLoginId) + .nickname(nickname) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupDetailResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupDetailResponseDto.java index 583a510d..704e46ae 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupDetailResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupDetailResponseDto.java @@ -1,3 +1,37 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eab6bac99ea5b0de6829c6228862aeb77f91f586f668c49b5c5bebb372506b46 -size 1261 +package com.dinnertime.peaktime.domain.group.service.dto.response; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.timer.service.dto.response.TimerItemResponseDto; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GroupDetailResponseDto { + + private String title; + private Long presetId; + private String presetTitle; + private List timerList; + + @Builder + private GroupDetailResponseDto(String title, Long presetId, String presetTitle, List timerList) { + this.title = title; + this.presetId = presetId; + this.presetTitle = presetTitle; + this.timerList = timerList; + } + + public static GroupDetailResponseDto createGroupDetailResponseDto(Group group, List timerList) { + return GroupDetailResponseDto.builder() + .title(group.getTitle()) + .presetId(group.getPreset().getPresetId()) + .presetTitle(group.getPreset().getTitle()) + .timerList(timerList) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupItemResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupItemResponseDto.java index 4d5783ed..5f110f73 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupItemResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupItemResponseDto.java @@ -1,3 +1,32 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b263275270fa5c6196dded71bb95f15407c76984468b95ffe5eccdedf9aeecc -size 965 +package com.dinnertime.peaktime.domain.group.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GroupItemResponseDto { + + private Long groupId; + private String groupTitle; + private List childList; + + @Builder + private GroupItemResponseDto(Long groupId, String groupTitle, List childList) { + this.groupId = groupId; + this.groupTitle = groupTitle; + this.childList = childList; + } + + public static GroupItemResponseDto createGroupItemResponseDto(Long groupId, String groupTitle, List childList) { + return GroupItemResponseDto.builder() + .groupId(groupId) + .groupTitle(groupTitle) + .childList(childList) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupListResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupListResponseDto.java index 7f44a1c0..ceb89147 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupListResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/group/service/dto/response/GroupListResponseDto.java @@ -1,3 +1,26 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be0951b88dbbff2343575dda234630f7793d99e6d68a2a7b18b87e266be8d720 -size 698 +package com.dinnertime.peaktime.domain.group.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GroupListResponseDto { + + private List groupList; + + @Builder + private GroupListResponseDto(List groupList) { + this.groupList = groupList; + } + + public static GroupListResponseDto createGroupListResponseDto(List groupList) { + return GroupListResponseDto.builder() + .groupList(groupList) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/controller/HikingController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/controller/HikingController.java index 4f2124a6..b3ed9dcd 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/controller/HikingController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/controller/HikingController.java @@ -1,3 +1,153 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b1179537e53588154a9d6de64fc7dbdd43f15622703837871f48a5e5bda7ca3 -size 8734 +package com.dinnertime.peaktime.domain.hiking.controller; + +import com.dinnertime.peaktime.domain.hiking.service.HikingService; +import com.dinnertime.peaktime.domain.hiking.service.dto.request.EndHikingRequestDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.request.StartHikingRequestDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.response.*; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/hikings") +public class HikingController { + + private final HikingService hikingService; + + @Operation(summary = "하이킹 시작", description = "하이킹 시작하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "하이킹을 시작하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = StartHikingResponseDto.class)) + ), + @ApiResponse(responseCode = "400", description = "집중 시간은 최대 4시간을 초과 할 수 없습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "하이킹을 시작하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping + public ResponseEntity startHiking(@AuthenticationPrincipal UserPrincipal userPrincipal, @RequestBody @Valid StartHikingRequestDto requestDto) { + + log.info("하이킹 시작"); + + StartHikingResponseDto responseDto = hikingService.startHiking(userPrincipal.getUserId(), requestDto); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "하이킹을 시작하는데 성공하였습니다.", responseDto)); + } + + @Operation(summary = "하이킹 종료", description = "하이킹 종료하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "하이킹을 종료하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "400", description = "컨텐츠 타입은 'program' 또는 'site' 여야 합니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "403", description = "자식 계정은 하이킹 중 종료 할 수 없습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "하이킹을 종료하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/{hiking-id}") + public ResponseEntity endHiking(@RequestBody @Valid EndHikingRequestDto requestDto, @PathVariable("hiking-id") Long hikingId) { + hikingService.endHiking(requestDto, hikingId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "하이킹을 종료하는데 성공하였습니다.")); + } + + @Operation(summary = "하이킹 캘린더", description = "하이킹 캘린더 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "하이킹 캘린더를 조회하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = HikingCalendarResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "하이킹 캘린더를 조회하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/calendar") + public ResponseEntity getCalendar(@AuthenticationPrincipal UserPrincipal userPrincipal) { + HikingCalendarResponseDto responseDto = hikingService.getCalendar(userPrincipal.getUserId()); + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "하이킹 캘린더를 조회하는데 성공하였습니다.", responseDto)); + } + + @Operation(summary = "하이킹 캘린더 상세 조회", description = "하이킹 캘린더 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "하이킹 캘린더 상세를 조회하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = HikingCalendarDetailResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "하이킹 캘린더 상세를 조회하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping(value = "/calendar/date/{date}") + public ResponseEntity getCalendarByDate( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @PathVariable("date") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) { + HikingCalendarDetailResponseDto responseDto = hikingService.getCalendarByDate(userPrincipal.getUserId(), date); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "하이킹 내역 목록을 조회하는데 성공하였습니다.", responseDto)); + } + + @Operation(summary = "하이킹 내역 상세 조회", description = "하이킹 내역 상세 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "하이킹 내역 상세를 조회하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = HikingDetailResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "하이킹 내역 상세를 조회하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping(value = "/{hiking-id}") + public ResponseEntity getHikingDetail(@PathVariable("hiking-id") Long hikingId) { + + HikingDetailResponseDto responseDto = hikingService.getHikingDetail(hikingId); + + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "하이킹 내역 상세를 조회하는데 성공하였습니다.", responseDto)); + } + + @Operation(summary = "하이킹 통계 내역 조회", description = "하이킹 통계 내역 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "하이킹 통계 내역을 조회하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = HikingStatisticWrapperResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "하이킹 통계 내역을 조회하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping(value = "/statistics") + public ResponseEntity getHikingStatistics(@AuthenticationPrincipal UserPrincipal userPrincipal) { + + HikingStatisticResponseDto responseDto = hikingService.getHikingStatistic(userPrincipal.getUserId()); + +// log.info(responseDto.toString()); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "하이킹 내역 상세를 조회하는데 성공하였습니다.", responseDto)); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/entity/Hiking.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/entity/Hiking.java index e30fa5f0..c6527f36 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/entity/Hiking.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/entity/Hiking.java @@ -1,3 +1,66 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:790a347f7361677283082b63b49b516e863caa35d8d67ca67d075a2132bca8b6 -size 2163 +package com.dinnertime.peaktime.domain.hiking.entity; + +import com.dinnertime.peaktime.domain.hiking.service.dto.request.StartHikingRequestDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "hikings") +public class Hiking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "hiking_id") + private Long hikingId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss", timezone="Asia/Seoul") + @Column(name = "start_time", nullable = false) + private LocalDateTime startTime; + + @JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss", timezone="Asia/Seoul") + @Column(name = "end_time", nullable = false) + private LocalDateTime endTime; + + @Column(name = "is_self", nullable = false) + private Boolean isSelf; + + @JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss", timezone="Asia/Seoul") + @Column(name = "real_end_time") + private LocalDateTime realEndTime; + + @Builder + private Hiking(User user, LocalDateTime startTime, LocalDateTime endTime, Boolean isSelf, LocalDateTime realEndTime) { + this.user = user; + this.startTime = startTime; + this.endTime = endTime; + this.isSelf = isSelf; + this.realEndTime = realEndTime; + } + + public static Hiking createHiking(User user, StartHikingRequestDto requestDto, LocalDateTime endTime) { + + return Hiking.builder() + .user(user) + .startTime(requestDto.getStartTime()) + .endTime(endTime) + .isSelf(requestDto.getIsSelf()) + .build(); + } + + public void updateRealEndTime(LocalDateTime realEndTime) { + this.realEndTime = realEndTime; + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryCustom.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryCustom.java index 90388a34..a85956dc 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryCustom.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryCustom.java @@ -1,3 +1,27 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:959b2372515bf59b1b322aedeb846509e498886a25480e84e967aea5da641fde -size 1010 +package com.dinnertime.peaktime.domain.hiking.repository; + +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingCalendarDetailQueryDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingCalendarQueryDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingDetailQueryDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingStatisticQueryDto; +import com.dinnertime.peaktime.domain.user.entity.User; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +public interface HikingRepositoryCustom { + List getCalendar(Long userId); + + List getCalendarByDate(LocalDate date, Long userId); + + HikingDetailQueryDto getHikingDetail(Long hikingId); + + HikingStatisticQueryDto getHikingStatistic(Long findUserId); + + List getStartTimeListByUserId(Long userId); + + Long getTotalBlockedCount(Long findUserId); + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryImpl.java index fc5fc422..2c14ab94 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/repository/HikingRepositoryImpl.java @@ -1,3 +1,147 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72f18844d4572086c5c293a365edeb6adbbbac4a653df63f433eda759f572642 -size 6255 +package com.dinnertime.peaktime.domain.hiking.repository; + +import com.dinnertime.peaktime.domain.content.entity.QContent; +import com.dinnertime.peaktime.domain.hiking.entity.QCalendar; +import com.dinnertime.peaktime.domain.hiking.entity.QHiking; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.*; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.sql.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class HikingRepositoryImpl implements HikingRepositoryCustom { + + private final QHiking hiking = QHiking.hiking; + private final QCalendar calendar = QCalendar.calendar; + private final QContent content = QContent.content; + private final JPAQueryFactory queryFactory; + + // 날짜만 가져오기 + @Override + public List getCalendar(Long userId) { + //날짜별로 하이킹 시간 조회 + return queryFactory.select(Projections.fields( + HikingCalendarQueryDto.class, + calendar.date.as("date"), + //없는 경우 0을 출력 + Expressions.numberTemplate(Integer.class, + "COALESCE(FLOOR(SUM((EXTRACT(EPOCH FROM {0}) - EXTRACT(EPOCH FROM {1})) / 60)), 0)", + hiking.realEndTime, hiking.startTime + ).as("totalMinute") + )) + .from(calendar) + .leftJoin(hiking) + .on( + calendar.date.eq(Expressions.dateTemplate(Date.class, "DATE_TRUNC('day', {0})", hiking.startTime)) + .and(hiking.user.userId.eq(userId)) + .and(hiking.realEndTime.isNotNull()) + .and(hiking.realEndTime.after(hiking.endTime)) + ) + .groupBy(calendar.date) + .orderBy(calendar.date.asc()) + .fetch(); + } + + @Override + public List getCalendarByDate(LocalDate date, Long userId) { + + return queryFactory.select(Projections.fields( + HikingCalendarDetailQueryDto.class, + hiking.hikingId.as("hikingId"), + hiking.startTime.as("startTime"), + hiking.endTime.as("endTime"), + hiking.realEndTime.as("realEndTime") + )) + .from(hiking) + .where( + hiking.user.userId.eq(userId) + .and(hiking.realEndTime.isNotNull()) + .and(Expressions.dateTemplate(LocalDate.class, "DATE({0})", hiking.startTime).eq(date)) + ) + .orderBy(hiking.startTime.asc()) + .fetch(); + } + + @Override + public HikingDetailQueryDto getHikingDetail(Long hikingId) { + // 주요 Hiking 정보 가져오기 + + return queryFactory.select(Projections.fields( + HikingDetailQueryDto.class, + hiking.startTime.as("startTime"), + hiking.endTime.as("endTime"), + hiking.realEndTime.as("realEndTime"), + // 조건부 합계를 위한 blockedSiteCount + new CaseBuilder() + .when(content.isBlocked.isTrue().and(content.type.eq("site"))).then(1) + .otherwise(0) + .sum().as("blockedSiteCount") + , + new CaseBuilder() + .when(content.isBlocked.isTrue().and(content.type.eq("program"))).then(1) + .otherwise(0) + .sum().as("blockedProgramCount") + )) + .from(hiking) + .join(content) + .on(hiking.hikingId.eq(content.hiking.hikingId)) + .where(hiking.hikingId.eq(hikingId)) + .groupBy(hiking.hikingId) + .fetchOne(); + } + + + @Override + public HikingStatisticQueryDto getHikingStatistic(Long findUserId) { + + //통계 조회 + return queryFactory.select(Projections.fields( + HikingStatisticQueryDto.class, + Expressions.numberTemplate(Integer.class, + "COALESCE(FLOOR(SUM((EXTRACT(EPOCH FROM {0}) - EXTRACT(EPOCH FROM {1})) / 60)), 0)", + hiking.realEndTime, hiking.startTime + ).as("totalHikingTime"), + hiking.hikingId.count().as("totalHikingCount"), + new CaseBuilder() + .when(hiking.realEndTime.after(hiking.endTime)).then(1) + .otherwise(0) + .sum().as("totalHikingSuccessCount") + )) + .from(hiking) + .where(hiking.user.userId.eq(findUserId).and(hiking.realEndTime.isNotNull())) + .groupBy(hiking.user.userId) + .fetchOne(); + } + + @Override + public List getStartTimeListByUserId(Long userId) { + return queryFactory.select(hiking.startTime) + .from(hiking) + .where( + hiking.user.userId.eq(userId) + .and(hiking.realEndTime.isNotNull()) + .and(hiking.realEndTime.after(hiking.endTime)) + ) + .fetch(); + } + + @Override + public Long getTotalBlockedCount(Long findUserId) { + return queryFactory.select(content.count()) + .from(content) + .where(content.hiking.user.userId.eq(findUserId).and(content.isBlocked.isTrue())) + .fetchOne(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/HikingService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/HikingService.java index c803dd9a..c296eba9 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/HikingService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/HikingService.java @@ -1,3 +1,207 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d5bd1aaa82c934d9e8d5b8b0ddb8a5326136e6e7cccaf65c8e2dd9d1891179b5 -size 9550 +package com.dinnertime.peaktime.domain.hiking.service; + +import com.dinnertime.peaktime.domain.content.entity.Content; +import com.dinnertime.peaktime.domain.content.repository.ContentRepository; +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.hiking.entity.Hiking; +import com.dinnertime.peaktime.domain.hiking.repository.HikingRepository; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingCalendarDetailQueryDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingCalendarQueryDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingDetailQueryDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.request.EndHikingRequestDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.request.StartHikingRequestDto; +import com.dinnertime.peaktime.domain.hiking.service.dto.response.*; +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import com.dinnertime.peaktime.domain.statistic.entity.StatisticContent; +import com.dinnertime.peaktime.domain.statistic.repository.StatisticRepository; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.RedisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class HikingService { + private final HikingRepository hikingRepository; + private final UserRepository userRepository; + private final ContentRepository contentRepository; + private final UserGroupRepository userGroupRepository; + private final RedisService redisService; + private final StatisticRepository statisticRepository; + private final GroupRepository groupRepository; + + private static final int DAY = 7; + private static final int DAY_MINUTE = 1440; + + @Transactional + public StartHikingResponseDto startHiking(Long userId, StartHikingRequestDto requestDto) { + //유저 없으면 에러 + User user = userRepository.findByUserIdAndIsDeleteFalse(userId).orElseThrow( + () -> new CustomException(ErrorCode.USER_NOT_FOUND) + ); + LocalDateTime startTime = requestDto.getStartTime(); + + //자식계정이고 스스로 했을 경우 + if(!user.getIsRoot() && requestDto.getIsSelf()) { + UserGroup userGroup = userGroupRepository.findByUser_UserId(userId).orElseThrow( + () -> new CustomException(ErrorCode.GROUP_NOT_FOUND) + ); + //검증 + //월요일이 1 일요일이 7 -> 일요일이 0, 월이 6으로 변경 + int day = DAY - startTime.getDayOfWeek().getValue(); + //분 구함 + int minute = (startTime.getHour() * 60) + startTime.getMinute(); + //레디스 저장된 시간으로 구하기 + int start = minute + (day * DAY_MINUTE); + //child계정이 속한 그룹의 타이머에 속하는지 확인 + boolean checkDuplicated = redisService.checkTimerByGroupId(userGroup.getGroup().getGroupId(), start, start + requestDto.getAttentionTime()); + if (checkDuplicated) { + throw new CustomException(ErrorCode.TIME_SLOT_OVERLAP); + } + } + + //시작시간에 집중 시간을 더하여 endTime을 구함 + LocalDateTime endTime = startTime.plusMinutes(requestDto.getAttentionTime()); + + Hiking hiking = Hiking.createHiking(user, requestDto, endTime); + hikingRepository.save(hiking); + + return StartHikingResponseDto.createStartHikingResponseDto(hiking.getHikingId()); + } + + @Transactional + public void endHiking(EndHikingRequestDto requestDto, Long hikingId) { + + //하이킹 조회 + Hiking hiking = hikingRepository.findByHikingId(hikingId).orElseThrow( + () -> new CustomException(ErrorCode.HIKING_NOT_FOUND) + ); + + //본인이 하이킹 하지 않고 실제 종료 시간이 종료 시간보다 작으면 에러 + if(!hiking.getIsSelf() && hiking.getEndTime().isAfter(requestDto.getRealEndTime())) { + throw new CustomException(ErrorCode.CHILD_ACCOUNT_HIKING_NOT_TERMINABLE); + } + + //하이킹 실제 종료 시간 업데이트 + hiking.updateRealEndTime(requestDto.getRealEndTime()); + hikingRepository.save(hiking); + + //없으면 조기 종료 + if(requestDto.getContentList()==null) return; + + //접속 기록 저장 + List contentList = requestDto.getContentList() + .stream() + .map(contentListRequestDto -> Content.createContent(hiking, contentListRequestDto)) + .toList(); + contentRepository.saveAll(contentList); + } + + @Transactional(readOnly = true) + public HikingCalendarResponseDto getCalendar(Long userId) { + + //날짜별로 누적 시간 합치기 + List calendarList = hikingRepository.getCalendar(userId); + + HikingCalendarResponseDto responseDto = HikingCalendarResponseDto.createHikingCalenderResponseDto(calendarList); + + log.info(calendarList.toString()); + + return responseDto; + } + + @Transactional(readOnly = true) + public HikingCalendarDetailResponseDto getCalendarByDate(Long userId, LocalDate date) { + + List calendarByDateList = hikingRepository.getCalendarByDate(date, userId); + + log.info(calendarByDateList.toString()); + + return HikingCalendarDetailResponseDto.createHikingCalendarDetailResponseDto(calendarByDateList); + } + + @Transactional(readOnly = true) + public HikingDetailResponseDto getHikingDetail(Long hikingId) { + + //디테일 조회 + HikingDetailQueryDto hikingDetail = hikingRepository.getHikingDetail(hikingId); + + Hiking hiking = hikingRepository.findByHikingId(hikingId).orElseThrow( + () -> new CustomException(ErrorCode.HIKING_NOT_FOUND) + ); + //없으면 null 반환 + if(hikingDetail==null) return HikingDetailResponseDto.noHikingDetail(hiking); + + List visitedSiteList = contentRepository.getTopUsingInfoList("site", hikingId); + List visitedProgramList = contentRepository.getTopUsingInfoList("program", hikingId); + + hikingDetail.setVisitedSiteList(visitedSiteList); + hikingDetail.setVisitedProgramList(visitedProgramList); + + + return HikingDetailResponseDto.createHikingDetailResponseDto(hikingDetail); + + } + + @Transactional(readOnly = true) + public HikingStatisticResponseDto getHikingStatistic(Long userId) { + + User user = userRepository.findByUserId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 자식 계정일 경우 + if (!user.getIsRoot()) { + return createChildStatisticResponse(user); + } + + // 루트 계정일 경우 + return createRootStatisticResponse(user); + } + + //자식계정 생성 + private HikingStatisticResponseDto createChildStatisticResponse(User user) { + Optional statistic = statisticRepository.findByUser_UserId(user.getUserId()); + HikingStatisticWrapperResponseDto root = statistic + .map(HikingStatisticWrapperResponseDto::createHikingStatisticResponseDto) + .orElseGet(() -> HikingStatisticWrapperResponseDto.createNoHiking(user)); + return HikingStatisticResponseDto.createChildHikingStatisticResponseDto(root); + } + + //루트계정 생성 + private HikingStatisticResponseDto createRootStatisticResponse(User user) { + Optional rootStatistic = statisticRepository.findByUser_UserId(user.getUserId()); + HikingStatisticWrapperResponseDto root = rootStatistic + .map(HikingStatisticWrapperResponseDto::createHikingStatisticResponseDto) + .orElseGet(() -> HikingStatisticWrapperResponseDto.createNoHiking(user)); + + List responseDtoList = groupRepository.findByUser_UserIdAndIsDeleteFalse(user.getUserId()) + .stream() + .map(group -> { + List userList = groupRepository.findUserListByGroupId(group.getGroupId()); + List statisticList = statisticRepository.findAllByUserIn(userList); + List wrapperList = statisticList.stream() + .map(HikingStatisticWrapperResponseDto::createHikingStatisticResponseDto) + .toList(); + return HikingGroupStatisticResponseDto.createGroupResponseDto(group, wrapperList); + }) + .toList(); + + return HikingStatisticResponseDto.createRootHikingStatisticResponseDto(root, responseDtoList); + } + +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingCalendarDetailQueryDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingCalendarDetailQueryDto.java index b94e4b9b..23573268 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingCalendarDetailQueryDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingCalendarDetailQueryDto.java @@ -1,3 +1,25 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64883a2a5266f40e0d620566dfdf2c3ab3a84c417e0ed74d41d96b3cea7d7aab -size 707 +package com.dinnertime.peaktime.domain.hiking.service.dto.query; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDateTime; + + +@Getter +@ToString +public class HikingCalendarDetailQueryDto { + + private Long hikingId; + + //백-> 프론트로 json으로 보낼때 직렬화 + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime realEndTime; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingDetailQueryDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingDetailQueryDto.java index a5eee8f1..a0cfdabe 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingDetailQueryDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/query/HikingDetailQueryDto.java @@ -1,3 +1,32 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54a0c58f61d535c68cb1aa7f55876c6c3f6db4d9bd9580d6956304d4e1627cf7 -size 894 +package com.dinnertime.peaktime.domain.hiking.service.dto.query; + +import com.dinnertime.peaktime.domain.statistic.entity.StatisticContent; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@ToString +public class HikingDetailQueryDto { + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime realEndTime; + + private Integer blockedSiteCount; + + private Integer blockedProgramCount; + + @Setter + private List visitedSiteList; + + @Setter + private List visitedProgramList; +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/ContentListRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/ContentListRequestDto.java index 1498d516..0a6cd7cc 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/ContentListRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/ContentListRequestDto.java @@ -1,3 +1,27 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b5b87e30d6a5d427ae3183028e76f8d9533a6be25f16b57ed44e285a0f5f8d88 -size 676 +package com.dinnertime.peaktime.domain.hiking.service.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ContentListRequestDto { + + @NotNull + private String contentName; + + @NotNull + @Pattern(regexp = "program|site", message = "컨텐츠 타입은 'program' 또는 'site' 여야 합니다.") + private String contentType; + + @NotNull + private Integer usingTime; + + @NotNull + private Boolean isBlockContent; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/EndHikingRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/EndHikingRequestDto.java index 82f9d813..101424c6 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/EndHikingRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/EndHikingRequestDto.java @@ -1,3 +1,23 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e6a62771d68e3aa296f3d1f345e1fe43b42b35e873b55278ff6296b18003b9 -size 602 +package com.dinnertime.peaktime.domain.hiking.service.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EndHikingRequestDto { + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime realEndTime; + + @Valid + private List contentList; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/StartHikingRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/StartHikingRequestDto.java index fca30903..d0b81060 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/StartHikingRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/request/StartHikingRequestDto.java @@ -1,3 +1,27 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e95d02ef53c7ff9cd961ed36148d5e52796bd2ca70f22bdff482153580e61a84 -size 762 +package com.dinnertime.peaktime.domain.hiking.service.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StartHikingRequestDto { + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @NotNull + @Max(value = 240, message = "집중 시간은 최대 4시간을 초과 할 수 없습니다.") + private Integer attentionTime; + + @NotNull + private Boolean isSelf; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarDetailResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarDetailResponseDto.java index cbc99dc8..0c71aec0 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarDetailResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarDetailResponseDto.java @@ -1,3 +1,26 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a5ba064a566f1beed32fbaf7a21f3ce6a863fb42d9669b0706e58d32016341c -size 919 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingCalendarDetailQueryDto; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HikingCalendarDetailResponseDto { + private List hikingDetailList; + + @Builder + private HikingCalendarDetailResponseDto(List hikingDetailList) { + this.hikingDetailList = hikingDetailList; + } + + public static HikingCalendarDetailResponseDto createHikingCalendarDetailResponseDto(List hikingDetailList) { + return HikingCalendarDetailResponseDto.builder() + .hikingDetailList(hikingDetailList) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarResponseDto.java index c72633d7..b05d417f 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingCalendarResponseDto.java @@ -1,3 +1,26 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22fb2026241e0fe23336b1a18003d8f5cd8086939e8043b965138405b2026504 -size 823 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingCalendarQueryDto; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HikingCalendarResponseDto { + private List hikingList; + + @Builder + private HikingCalendarResponseDto(List hikingList) { + this.hikingList = hikingList; + } + + public static HikingCalendarResponseDto createHikingCalenderResponseDto(List hikingList) { + return HikingCalendarResponseDto.builder() + .hikingList(hikingList) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingDetailResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingDetailResponseDto.java index 44b2df29..92e11e67 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingDetailResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingDetailResponseDto.java @@ -1,3 +1,72 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ae273e1729d8667c3bcf6cd07ff14ba1bc2286d9689c4a07e0fa80505c0f273 -size 2922 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import com.dinnertime.peaktime.domain.hiking.entity.Hiking; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingDetailQueryDto; +import com.dinnertime.peaktime.domain.statistic.entity.StatisticContent; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@ToString +@Slf4j +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HikingDetailResponseDto { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime realEndTime; + + private int blockedSiteCount; + + private int blockedProgramCount; + + private List visitedSiteList; + + private List visitedProgramList; + + @Builder + private HikingDetailResponseDto(LocalDateTime startTime, LocalDateTime endTime, LocalDateTime realEndTime, int blockedSiteCount, int blockedProgramCount, List visitedSiteList, List visitedProgramList) { + this.startTime = startTime; + this.endTime = endTime; + this.realEndTime = realEndTime; + this.blockedSiteCount = blockedSiteCount; + this.blockedProgramCount = blockedProgramCount; + this.visitedSiteList = visitedSiteList; + this.visitedProgramList = visitedProgramList; + } + + public static HikingDetailResponseDto createHikingDetailResponseDto(HikingDetailQueryDto hikingDetailQueryDto) { + + return HikingDetailResponseDto.builder() + .startTime(hikingDetailQueryDto.getStartTime()) + .endTime(hikingDetailQueryDto.getEndTime()) + .realEndTime(hikingDetailQueryDto.getRealEndTime()) + .blockedSiteCount(hikingDetailQueryDto.getBlockedSiteCount()) + .blockedProgramCount(hikingDetailQueryDto.getBlockedProgramCount()) + .visitedSiteList(hikingDetailQueryDto.getVisitedSiteList()) + .visitedProgramList(hikingDetailQueryDto.getVisitedProgramList()) + .build(); + } + + public static HikingDetailResponseDto noHikingDetail(Hiking hiking) { + return HikingDetailResponseDto.builder() + .startTime(hiking.getStartTime()) + .endTime(hiking.getEndTime()) + .realEndTime(hiking.getRealEndTime()) + .blockedSiteCount(0) + .blockedProgramCount(0) + .visitedSiteList(new ArrayList<>()) + .visitedProgramList(new ArrayList<>()) + .build(); + } + +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingGroupStatisticResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingGroupStatisticResponseDto.java index 110d6626..bee91525 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingGroupStatisticResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingGroupStatisticResponseDto.java @@ -1,3 +1,35 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc71d384c604a04401f91f854a275b2eefbe6b90f88e2f5e9298a76fb181b8b1 -size 1183 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HikingGroupStatisticResponseDto { + private Long groupId; + private String groupTitle; + private List childList; + + @Builder + private HikingGroupStatisticResponseDto(Long groupId, String groupTitle, List childList) { + this.groupId = groupId; + this.groupTitle = groupTitle; + this.childList = childList; + } + + public static HikingGroupStatisticResponseDto createGroupResponseDto(Group group, List childList) { + return HikingGroupStatisticResponseDto + .builder() + .groupId(group.getGroupId()) + .groupTitle(group.getTitle()) + .childList(childList) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticResponseDto.java index ee243a80..7b5da403 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticResponseDto.java @@ -1,3 +1,38 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73e1d167896b876c13b88342eed87a53367d53f5f8157eefbb641fedb849d595 -size 1323 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HikingStatisticResponseDto { + private HikingStatisticWrapperResponseDto root; + private List groupList; + + @Builder + private HikingStatisticResponseDto(HikingStatisticWrapperResponseDto root, List groupList) { + this.root = root; + this.groupList = groupList; + } + + //루트 계정일 경우 + public static HikingStatisticResponseDto createRootHikingStatisticResponseDto(HikingStatisticWrapperResponseDto root, List groupList) { + return HikingStatisticResponseDto.builder() + .root(root) + .groupList(groupList) + .build(); + } + + //자식 계정일 경우 + public static HikingStatisticResponseDto createChildHikingStatisticResponseDto(HikingStatisticWrapperResponseDto root) { + return HikingStatisticResponseDto.builder() + .root(root) + .groupList(new ArrayList<>()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticWrapperResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticWrapperResponseDto.java index 4c3d4cbb..5a67f8b5 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticWrapperResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/HikingStatisticWrapperResponseDto.java @@ -1,3 +1,77 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3cc64027450d5b02f52b555abf58408cd444905f14331277a9513bf2ff2ba46b -size 2954 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import com.dinnertime.peaktime.domain.statistic.entity.StatisticContent; +import com.dinnertime.peaktime.domain.user.entity.User; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HikingStatisticWrapperResponseDto { + + private Long userId; + + private String nickname; + + private Integer totalHikingTime; + + private Long totalHikingCount; + + private Integer totalSuccessCount; + + private Long totalBlockedCount; + +// private int preferTimeZone; + private List startTimeList; + + private List mostSiteList; + + private List mostProgramList; + + @Builder + private HikingStatisticWrapperResponseDto(Long userId, String nickname, Integer totalHikingTime, Long totalHikingCount, Integer totalSuccessCount, Long totalBlockedCount, List startTimeList, List mostSiteList, List mostProgramList) { + this.userId = userId; + this.nickname = nickname; + this.totalHikingTime = totalHikingTime; + this.totalHikingCount = totalHikingCount; + this.totalSuccessCount = totalSuccessCount; + this.totalBlockedCount = totalBlockedCount; + this.startTimeList = startTimeList; + this.mostSiteList = mostSiteList; + this.mostProgramList = mostProgramList; + } + + public static HikingStatisticWrapperResponseDto createHikingStatisticResponseDto(Statistic statistic) { + return HikingStatisticWrapperResponseDto.builder() + .userId(statistic.getUser().getUserId()) + .nickname(statistic.getUser().getNickname()) + .totalHikingTime(statistic.getTotalHikingTime()) + .totalHikingCount(statistic.getTotalHikingCount()) + .totalSuccessCount(statistic.getTotalSuccessCount()) + .totalBlockedCount(statistic.getTotalBlockCount()) + .startTimeList(statistic.getStartTimeArray()) + .mostSiteList(statistic.getMostSiteArray()) + .mostProgramList(statistic.getMostProgramArray()) + .build(); + } + + public static HikingStatisticWrapperResponseDto createNoHiking(User user) { + return HikingStatisticWrapperResponseDto.builder() + .userId(user.getUserId()) + .nickname(user.getNickname()) + .totalHikingTime(0) + .totalHikingCount(0L) + .totalSuccessCount(0) + .totalBlockedCount(0L) + .startTimeList(new ArrayList<>()) + .mostSiteList(new ArrayList<>()) + .mostProgramList(new ArrayList<>()) + .build(); + } +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/StartHikingResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/StartHikingResponseDto.java index 0262d139..d4b8c4df 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/StartHikingResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/hiking/service/dto/response/StartHikingResponseDto.java @@ -1,3 +1,23 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92739508dd3f862c0ee5fa638dbf0dc26359b50c38734985dc849c60e77de8b4 -size 588 +package com.dinnertime.peaktime.domain.hiking.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StartHikingResponseDto { + private Long hikingId; + + @Builder + private StartHikingResponseDto(Long hikingId) { + this.hikingId = hikingId; + } + + public static StartHikingResponseDto createStartHikingResponseDto(Long hikingId) { + return builder() + .hikingId(hikingId) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/controller/MemoController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/controller/MemoController.java index a1a6dce5..e80c6b9c 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/controller/MemoController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/controller/MemoController.java @@ -1,3 +1,118 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:10d999fa6e2df6d3eae7d12f448bed7a7c0da62c30d6d354e246c63acd375cf4 -size 5686 +package com.dinnertime.peaktime.domain.memo.controller; + +import com.dinnertime.peaktime.domain.memo.service.MemoService; +import com.dinnertime.peaktime.domain.memo.service.dto.request.SaveMemoRequestDto; +import com.dinnertime.peaktime.domain.memo.service.dto.response.MemoDetailResponseDto; +import com.dinnertime.peaktime.domain.memo.service.dto.response.MemoWrapperResponseDto; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/memos") +@RequiredArgsConstructor +public class MemoController { + + private final MemoService memoService; + + // 메모 리스트 조회 + @Operation(summary = "메모 리스트 조회", description = "화면 진입 시 메모의 리스트를 확인함") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "메모 리스트 조회에 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "메모 리시트 조회에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping + public ResponseEntity getMemoTitles ( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestParam("page") int page) { + log.info("getMemoTitles 메서드가 호출되었습니다."); + + MemoWrapperResponseDto responseDto = memoService.getMemos(userPrincipal.getUserId(), page); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"메모 리스트 조회에 성공했습니다.", responseDto)); + } + + // 메모 상세 조회 + @Operation(summary = "메모 상세 조회", description = "메모 리스트 내 타이틀 클릭시 발생하는 상세 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "메모 상세 조회에 성공했습니다.", + content = @Content(schema= @Schema(implementation = MemoDetailResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "메모 상세 조회에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/{memoId}") + public ResponseEntity getMemoDetail (@PathVariable("memoId") Long memoId) { + log.info("getMemoDetail 메서드가 호출되었습니다."); + + MemoDetailResponseDto responseDto = memoService.getDetailedMemo(memoId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"메모 및 요약 상세 조회에 성공했습니다.", responseDto)); + } + + // 메모 삭제 + @Operation(summary = "메모 삭제", description = "저장된 메모를 삭제함") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "메모 삭제에 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "메모 삭제에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("/{memoId}") + public ResponseEntity deleteMemo(@PathVariable("memoId") Long memoId) { + log.info("deleteMemo 메서드가 호출되었습니다."); + + memoService.deleteMemo(memoId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"메모 삭제에 성공했습니다.")); + } + + // 메모 생성 + @Operation(summary = "ex에서 받은 메모 저장하기", description = "새로운 메모 생성하기") + @ApiResponses(value ={ + @ApiResponse(responseCode = "200", description = "새로운 메모 저장에 성공했습니다.", + content=@Content(schema = @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "새로운 메모 저장에 실패했습니다.", + content=@Content(schema = @Schema(implementation = ResultDto.class)) + ), + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping() + public ResponseEntity saveMemo( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @Valid @RequestBody SaveMemoRequestDto requestDto) { + + log.info("saveMemo 메서드가 호출되었습니다."); + + memoService.createMemo(userPrincipal.getUserId(), requestDto); + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "메모 생성에 성공했습니다.")); + + } + + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/entity/Memo.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/entity/Memo.java index 19cd48da..1cd5241d 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/entity/Memo.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/entity/Memo.java @@ -1,3 +1,53 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13cd9df6f3733353a845da0d5b4a95fe9b6047b4a332f666af11d9782ae73090 -size 1530 +package com.dinnertime.peaktime.domain.memo.entity; + +import com.dinnertime.peaktime.domain.memo.service.dto.request.SaveMemoRequestDto; +import com.dinnertime.peaktime.domain.summary.entity.Summary; +import com.dinnertime.peaktime.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.nio.file.attribute.UserPrincipal; +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name="memos") +public class Memo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="memo_id") + private Long memoId; + + @Column(name="title", nullable = false,length = 20) + private String title; + + @Column(name="create_at", nullable = false) + private LocalDateTime createAt; + + @Column(name = "content", columnDefinition = "TEXT", nullable = false) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="user_id", nullable = false) + private User user; + + @Builder + private Memo(String title, LocalDateTime createAt, String content, User user) { + this.title = title; + this.createAt = createAt; + this.content = content; + this.user = user; + } + + public static Memo createMemo(SaveMemoRequestDto requestDto, User user){ + return Memo.builder() + .title(requestDto.getTitle()) + .createAt(LocalDateTime.now()) + .content(requestDto.getContent()) + .user(user) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/repository/MemoRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/repository/MemoRepository.java index a82ae63b..49abec39 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/repository/MemoRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/repository/MemoRepository.java @@ -1,3 +1,21 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b50c94a2e3eec7751196f671f59d65a6f039021ed4cf81a55312a5df8f86dc9 -size 664 +package com.dinnertime.peaktime.domain.memo.repository; + + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface MemoRepository extends JpaRepository { + + // 메모리스트 조회시 사용 + Page findAllByUser_UserId(Long userId, Pageable pageable); + + // 메모 상세 조회, 메모 삭제에 사용 + Optional findByMemoId(Long memoId); +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/MemoService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/MemoService.java index 8ac0f282..8a496bb1 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/MemoService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/MemoService.java @@ -1,3 +1,82 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7565d8ce9bcd47b2814428f19f450678c35c327deb656425f9ca889b3de7dcf0 -size 3282 +package com.dinnertime.peaktime.domain.memo.service; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import com.dinnertime.peaktime.domain.memo.repository.MemoRepository; +import com.dinnertime.peaktime.domain.memo.service.dto.request.SaveMemoRequestDto; +import com.dinnertime.peaktime.domain.memo.service.dto.response.MemoDetailResponseDto; +import com.dinnertime.peaktime.domain.memo.service.dto.response.MemoWrapperResponseDto; +import com.dinnertime.peaktime.domain.summary.repository.SummaryRepository; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.RedisService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class MemoService { + + private final MemoRepository memoRepository; + private final UserRepository userRepository; + private final SummaryRepository summaryRepository; + + private final RedisService redisService; + // 메모 리스트 조회, 삭제 구현 + + // 메모 리스트 조회 + // UserPrincipal 임시 설정 -> 구현 후 일괄 수정 예정 + @Transactional(readOnly = true) + public MemoWrapperResponseDto getMemos(Long userId, int page) { + + Pageable pageable = PageRequest.of(page, 10); + + Page memos = memoRepository.findAllByUser_UserId(userId, pageable); + + log.info(memos.toString()); + + // redis에서 임시 저장되어있는 요약 횟수 가져오기 + Integer count = redisService.getGPTcount(userId); + + return MemoWrapperResponseDto.createMemoWrapperResponseDto(memos.getContent(), count, memos.isLast()); + } + + // 메모 삭제 + @Transactional + public void deleteMemo(Long memoId) { + // summary entity에 summary id와 memoId가 매칭이 된다면(존재한다면) 요약 먼저 제거 후 메모 제거 처리 진행 + Memo memo = memoRepository.findByMemoId(memoId) + .orElseThrow(()-> new CustomException(ErrorCode.MEMO_NOT_FOUND)); + memoRepository.delete(memo); + } + + // 메모 상세 조회 + @Transactional(readOnly = true) + public MemoDetailResponseDto getDetailedMemo(Long memoId) { + Memo memo = memoRepository.findByMemoId(memoId) + .orElseThrow(()-> new CustomException(ErrorCode.MEMO_NOT_FOUND)); + + return MemoDetailResponseDto.createMemoDetailResponse(memo); + } + + // ex에서 받은 메모 정보 저장 + @Transactional + public void createMemo(Long userId, SaveMemoRequestDto requestDto) { + + // userPrincipal.getUserId() + User user = userRepository.findByUserIdAndIsDeleteFalse(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + Memo memo = Memo.createMemo(requestDto, user); + memoRepository.save(memo); + } + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/request/SaveMemoRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/request/SaveMemoRequestDto.java index e283bd71..e4a6e40a 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/request/SaveMemoRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/request/SaveMemoRequestDto.java @@ -1,3 +1,21 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0aea1b18bdf3f1943d829c834cccad9c425a81a664f8995a967e74eb668c7fc -size 525 +package com.dinnertime.peaktime.domain.memo.service.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SaveMemoRequestDto { + + @Size(min = 2, max = 15, message = "제목은 2글자 이상 15글자 이하로 입력해주세요.") + @NotNull + private String title; + + @NotNull + private String content; + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoDetailResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoDetailResponseDto.java index b5f44267..d8cb8c4f 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoDetailResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoDetailResponseDto.java @@ -1,3 +1,42 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d022f5dce266891cb5633b5c7b60c26fb62abfe7dbea59ce7ed77cb73cd6786c -size 1248 +package com.dinnertime.peaktime.domain.memo.service.dto.response; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemoDetailResponseDto { + + // 메모 상세 정보 넣기 + private Long memoId; + private String title; + private LocalDateTime memoCreateAt; + private String memoContent; + + @Builder + private MemoDetailResponseDto(Long memoId, String title, LocalDateTime memoCreateAt, String memoContent, Long summaryId,String summaryContent, LocalDateTime summaryUpdateAt) { + this.memoId = memoId; + this.title = title; + this.memoCreateAt = memoCreateAt; + this.memoContent = memoContent; + } + + public static MemoDetailResponseDto createMemoDetailResponse(Memo memo) { + return MemoDetailResponseDto.builder() + .memoId(memo.getMemoId()) + .title(memo.getTitle()) + .memoCreateAt(memo.getCreateAt()) + .memoContent(memo.getContent()) + .build(); + } + + + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoResponseDto.java index 5004c041..4ffbdda0 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoResponseDto.java @@ -1,3 +1,37 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e2b66bf96f57379fc59092e2490b64fccf9e9d79bc06f98a76d1f253e7a7612 -size 1014 +package com.dinnertime.peaktime.domain.memo.service.dto.response; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemoResponseDto { + + // 메모 리스트 responseDto 작성 + private Long memoId; + private String title; + private LocalDateTime createdAt; + + @Builder + private MemoResponseDto(Long memoId, String title, LocalDateTime createdAt) { + this.memoId = memoId; + this.title = title; + this.createdAt = createdAt; + } + + // 메모리스트 (내용 제외) responseDto 작성 + public static MemoResponseDto createMemoResponseDto(Memo memo) { + return MemoResponseDto.builder() + .memoId(memo.getMemoId()) + .title(memo.getTitle()) + .createdAt(memo.getCreateAt()) + .build(); + } + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoWrapperResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoWrapperResponseDto.java index cb718189..89ae5244 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoWrapperResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/memo/service/dto/response/MemoWrapperResponseDto.java @@ -1,3 +1,35 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2069ed168e3ff204ddf039e50181b42733372a192f764468433e96373be888a -size 1128 +package com.dinnertime.peaktime.domain.memo.service.dto.response; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import lombok.*; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +public class MemoWrapperResponseDto { + private List memoList; + private Integer summaryCount; // 하루 gpt 사용 횟수 + private Boolean isLastPage; + + @Builder + private MemoWrapperResponseDto(List memoList, Integer summaryCount, Boolean isLastPage) { + this.memoList = memoList; + this.summaryCount = summaryCount; + this.isLastPage = isLastPage; + } + + public static MemoWrapperResponseDto createMemoWrapperResponseDto(List memos, Integer count, Boolean isLastPage) { + List responseDto = memos.stream() + .map(MemoResponseDto::createMemoResponseDto) + .toList(); + + return MemoWrapperResponseDto.builder() + .memoList(responseDto) + .summaryCount(count) + .isLastPage(isLastPage) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/controller/PresetController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/controller/PresetController.java index cafb6562..a60ac3ff 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/controller/PresetController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/controller/PresetController.java @@ -1,3 +1,172 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2baa753655e1b9f9b885e8122b60110b6fb180b32915dce918da4363118058d -size 8561 +package com.dinnertime.peaktime.domain.preset.controller; + +import com.dinnertime.peaktime.domain.preset.service.PresetService; +import com.dinnertime.peaktime.domain.preset.service.dto.request.AddUrlPresetRequestDto; +import com.dinnertime.peaktime.domain.preset.service.dto.request.SavePresetRequestDto; +import com.dinnertime.peaktime.domain.preset.service.dto.response.PresetResponseDto; +import com.dinnertime.peaktime.domain.preset.service.dto.response.PresetWrapperResponseDto; +import com.dinnertime.peaktime.domain.preset.service.dto.response.SaveUrlPresetResponseDto; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Path; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/presets") +@RequiredArgsConstructor +public class PresetController { + + private final PresetService presetService; + + // 프리셋 생성 + @Operation(summary = "프리셋 생성", description = "추가 버튼으로 프리셋 생성하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "프리셋 생성에 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "프리셋 생성에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping + public ResponseEntity createPreset( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @Valid @RequestBody SavePresetRequestDto requestDto) { + //단순한 데이터 형식과 길이에 대한 유효성 검증은 컨트롤러에서 처리 @Valid + log.info("createPreset 메서드가 호출되었습니다."); + log.info("프리셋 생성 : " + requestDto.toString()); + + presetService.createPreset(userPrincipal.getUserId(), requestDto); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"프리셋 생성에 성공했습니다.")); + } + + + + // 프리셋 조회 + @Operation(summary = "프리셋 전체 조회", description = "프리셋 전체 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "프리셋 전체 조회에 성공했습니다.", + content = @Content(schema= @Schema(implementation = PresetWrapperResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "프리셋 전체 조회에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping + public ResponseEntity getPreset(@AuthenticationPrincipal UserPrincipal userPrincipal) { + log.info("getPreset 메서드가 호출되었습니다."); + + PresetWrapperResponseDto responseDto = presetService.getPresets(userPrincipal.getUserId()); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"프리셋 전체 조회에 성공했습니다.", responseDto)); + } + + // 특정 프리셋 조회 + // 프리셋 조회 + @Operation(summary = "특정 프리셋 조회", description = "프리셋 상세 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "특정 프리셋 조회에 성공했습니다.", + content = @Content(schema= @Schema(implementation = PresetResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "특정 프리셋 조회에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/{presetId}") + public ResponseEntity getEachPreset(@AuthenticationPrincipal UserPrincipal userPrincipal, + @PathVariable Long presetId) { + log.info("getEachPreset 메서드가 호출되었습니다."); + + PresetResponseDto responseDto = presetService.getUniquePreset(userPrincipal.getUserId(), presetId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"특정 프리셋 조회에 성공했습니다.", responseDto)); + } + + // 프리셋 수정 + @Operation(summary = "특정 프리셋 수정하기", description = "프리셋 수정하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "해당 프리셋 수정에 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "프리셋 수정에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/{presetId}") + public ResponseEntity updatePreset( + @PathVariable Long presetId, + @Valid @RequestBody SavePresetRequestDto requestDto) { + + log.info("updatePreset 메서드가 호출되었습니다."); + + presetService.updatePreset(requestDto, presetId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"프리셋 수정에 성공했습니다.")); + } + + + + // 프리셋 삭제 + @Operation(summary = "특정 프리셋 삭제하기", description = "프리셋 삭제하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "해당 프리셋 삭제에 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "프리셋 수정에 삭제했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("/{presetId}") + public ResponseEntity deletePreset(@PathVariable Long presetId) { + + log.info("deletePreset 메서드가 호출되었습니다."); + + presetService.deletePreset(presetId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"프리셋 삭제에 성공했습니다.")); + } + + // 특정 프리셋 생성 + @Operation(summary = "익스텐션에서 프리셋 웹사이트 추가", description = "익스텐션에서 프리셋 웹사이트 추가해주기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "웹사이트를 프리셋에 추가하기에 성공했습니다.", + content = @Content(schema= @Schema(implementation = PresetResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "웹사이트를 프리셋에 추가하기에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/{presetId}") + public ResponseEntity addUrl( + @Valid @RequestBody AddUrlPresetRequestDto requestDto, + @PathVariable Long presetId) { + //단순한 데이터 형식과 길이에 대한 유효성 검증은 컨트롤러에서 처리 @Valid + log.info("addUrl 메서드가 호출되었습니다."); + + SaveUrlPresetResponseDto responseDto = presetService.addWebsitePreset(requestDto, presetId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"웹사이트를 프리셋에 추가하기에 성공했습니다.", responseDto)); + } + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/entity/Preset.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/entity/Preset.java index e6e829ec..ba742bae 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/entity/Preset.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/entity/Preset.java @@ -1,3 +1,82 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f49e5a1f2287fb15ce4bca1c4517ae83b5260a47b279d10a1662ac9437748a6 -size 2924 +package com.dinnertime.peaktime.domain.preset.entity; + +import com.dinnertime.peaktime.domain.preset.service.dto.request.AddUrlPresetRequestDto; +import com.dinnertime.peaktime.domain.preset.service.dto.request.SavePresetRequestDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; +import lombok.*; + +import org.hibernate.annotations.Type; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "presets") +public class Preset { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="preset_id") + private Long presetId; // bigint wrapper class Long과 연결시키기 + + @Column(name ="title",nullable=false) + private String title; + + @Type(JsonBinaryType.class) + @Column(name="block_website_array",nullable=false, columnDefinition = "jsonb") + private List blockWebsiteArray; + + @Type(JsonBinaryType.class) + @Column(name="block_program_array", nullable=false, columnDefinition = "jsonb") + private List blockProgramArray; + + @ManyToOne(fetch=FetchType.LAZY) // 지연 로딩 + @JoinColumn(name="user_id", nullable=false) + private User user; + + @Builder + private Preset(String title, List blockWebsiteArray, List blockProgramArray, User user) { + this.title = title; + this.blockWebsiteArray = blockWebsiteArray; + this.blockProgramArray = blockProgramArray; + this.user = user; + } + + // 프리셋 생성 + public static Preset createPreset(SavePresetRequestDto requestDto, User user) { + return Preset.builder() + .title(requestDto.getTitle()) + .blockWebsiteArray(Arrays.asList(requestDto.getBlockWebsiteList())) + .blockProgramArray(Arrays.asList(requestDto.getBlockProgramList())) + .user(user) + .build(); + } + + // 회원가입 시 자동생성되는 프리셋 생성 + public static Preset createDefaultPreset(ListblockWebsiteArray, List blockProgramArray, User user) { + return Preset.builder() + .title("기본 프리셋") + .blockWebsiteArray(blockWebsiteArray) + .blockProgramArray(blockProgramArray) + .user(user) + .build(); + } + + // 프리셋 수정 + public void updatePreset(SavePresetRequestDto requestDto) { + this.title = requestDto.getTitle(); + this.blockWebsiteArray = Arrays.asList(requestDto.getBlockWebsiteList()); + this.blockProgramArray = Arrays.asList(requestDto.getBlockProgramList()); + } + + // 웹사이트 프리셋 추가 + public void addWebsitePreset(AddUrlPresetRequestDto requestDto) { + this.blockWebsiteArray.add(requestDto.getUrl()); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/repository/PresetRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/repository/PresetRepository.java index 9599833f..32910ea0 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/repository/PresetRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/repository/PresetRepository.java @@ -1,3 +1,17 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c7ec49bf8a18f811c42c7413485ade2da54dc09c97de0f677452414c569c2ae3 -size 550 +package com.dinnertime.peaktime.domain.preset.repository; + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import com.dinnertime.peaktime.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PresetRepository extends JpaRepository { + + List findAllByUser_UserIdOrderByPresetIdAsc(Long userId); + + Optional findByPresetId(Long presetId); +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/PresetService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/PresetService.java index 0804a1c8..2b45d8ec 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/PresetService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/PresetService.java @@ -1,3 +1,155 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e35c0669346d1261c1e23e7562ca2a37316dbc3d007e321bf0b3f3176467d9c9 -size 6042 +package com.dinnertime.peaktime.domain.preset.service; + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import com.dinnertime.peaktime.domain.preset.repository.PresetRepository; +import com.dinnertime.peaktime.domain.preset.service.dto.request.AddUrlPresetRequestDto; +import com.dinnertime.peaktime.domain.preset.service.dto.request.SavePresetRequestDto; +import com.dinnertime.peaktime.domain.preset.service.dto.response.PresetResponseDto; +import com.dinnertime.peaktime.domain.preset.service.dto.response.PresetWrapperResponseDto; +import com.dinnertime.peaktime.domain.preset.service.dto.response.SaveUrlPresetResponseDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class PresetService { + + // preset CRUD 처리 + private final PresetRepository presetRepository; + private final UserRepository userRepository; + private final UserGroupRepository userGroupRepository; + + // preset 생성 + @Transactional + public void createPreset(Long userId, SavePresetRequestDto requestDto) { + + //임시로 1로 고정시키기 추후 수정 userPrincipal.getUserId()); + User user = userRepository.findByUserIdAndIsDeleteFalse(userId). + orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // userId = 1로 임의 설정 + Preset preset = Preset.createPreset(requestDto, user); + + presetRepository.save(preset); + } + + // preset 조회 + @Transactional(readOnly = true) + public PresetWrapperResponseDto getPresets(Long userId) { + + User user = userRepository.findByUserId(userId).orElseThrow( + () -> new CustomException(ErrorCode.USER_NOT_FOUND) + ); + + //유저가 자식계정인 경우 + if(!user.getIsRoot()) { + UserGroup userGroup = userGroupRepository.findByUser_UserId(userId).orElseThrow( + () -> new CustomException(ErrorCode.GROUP_NOT_FOUND) + ); + + Preset preset = userGroup.getGroup().getPreset(); + + return PresetWrapperResponseDto.buildPresetResponseDto(preset); + } + + List presets = presetRepository.findAllByUser_UserIdOrderByPresetIdAsc(userId); + + // userId를 뺀 나머지 데이터 Wrapper해서 적용 + return PresetWrapperResponseDto.buildPresetResponseDto(presets); + } + + // 특정 프리셋 조회하기 + // preset 조회 + @Transactional(readOnly = true) + public PresetResponseDto getUniquePreset(Long userId, Long presetId) { + + User user = userRepository.findByUserId(userId).orElseThrow( + () -> new CustomException(ErrorCode.USER_NOT_FOUND) + ); + + //유저가 자식계정인 경우 + if(!user.getIsRoot()) { + UserGroup userGroup = userGroupRepository.findByUser_UserId(userId).orElseThrow( + () -> new CustomException(ErrorCode.GROUP_NOT_FOUND) + ); + + Preset preset = userGroup.getGroup().getPreset(); + + return PresetResponseDto.createPresetResponse(preset); + } + + // 루트 계정인 경우 presetId로 접근해서 찾기 + Preset preset = presetRepository.findByPresetId(presetId).orElseThrow( + () -> new CustomException(ErrorCode.PRESET_NOT_FOUND) + ); + + // userId를 뺀 나머지 데이터 Wrapper해서 적용 + return PresetResponseDto.createPresetResponse(preset); + } + + + // preset 업데이트 + @Transactional + public void updatePreset(SavePresetRequestDto requestDto, Long presetId) { + + // userId = 1로 임의 설정 + Preset preset = presetRepository.findByPresetId(presetId) + .orElseThrow(() -> new CustomException(ErrorCode.PRESET_NOT_FOUND)); + + preset.updatePreset(requestDto); + + presetRepository.save(preset); + } + + // 프리셋 삭제 + @Transactional + public void deletePreset(Long presetId) { + Preset preset = presetRepository.findByPresetId(presetId) + .orElseThrow(() -> new CustomException(ErrorCode.PRESET_NOT_FOUND)); + + // 그룹에 있을 때 presetId가 존재하는 경우 데이터 무결성 위반 에러 발생함 -> 예외처리 진행 + try{ + presetRepository.delete(preset); + } catch(DataIntegrityViolationException e){ // 데이터 무결성 위반 exception + throw new CustomException(ErrorCode.FAILED_DELETE_PRESET_IN_GROUP); + } + + } + + // ex에서 받아온 특정 웹사이트 프리셋 추가 + @Transactional + public SaveUrlPresetResponseDto addWebsitePreset(AddUrlPresetRequestDto requestDto, Long presetId) { + + Preset preset = presetRepository.findByPresetId(presetId) + .orElseThrow(() -> new CustomException(ErrorCode.PRESET_NOT_FOUND)); + + boolean isExist = preset.getBlockWebsiteArray().stream() + .anyMatch(website -> website.equals(requestDto.getUrl())); + + if(isExist) { + throw new CustomException(ErrorCode.DUPLICATED_URL); + } + + preset.addWebsitePreset(requestDto); + + presetRepository.save(preset); + + Preset responsePreset = presetRepository.findByPresetId(presetId) + .orElseThrow(() -> new CustomException(ErrorCode.PRESET_NOT_FOUND)); + + return SaveUrlPresetResponseDto.createSaveUrlPresetResponseDto(responsePreset); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/request/SavePresetRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/request/SavePresetRequestDto.java index 7ce4b053..2866c0cb 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/request/SavePresetRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/request/SavePresetRequestDto.java @@ -1,3 +1,20 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4304ba1dc17bec39a6928ccd68c91445ba267f5cfab2ea51cd57d09d1e09489e -size 610 +package com.dinnertime.peaktime.domain.preset.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SavePresetRequestDto { + + @Size(min = 2, max = 8, message = "제목은 2글자 이상 8글자 이하로 입력해주세요.") + @NotNull + private String title; + + private String[] blockWebsiteList; + private String[] blockProgramList; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetResponseDto.java index eda33a9c..790fde65 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetResponseDto.java @@ -1,3 +1,45 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:160ddac6aa3ea0c261016b0f3627a9e4ce989fd75b9705b2dcbc91dff614f498 -size 1428 +package com.dinnertime.peaktime.domain.preset.service.dto.response; + + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import lombok.*; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PresetResponseDto { + private Long presetId; + private String title; + private List blockWebsiteArray; + private List blockProgramArray; + + @Builder + private PresetResponseDto(Long presetId, String title, List blockWebsiteArray, List blockProgramArray) { + this.presetId = presetId; + this.title = title; + this.blockWebsiteArray = blockWebsiteArray; + this.blockProgramArray = blockProgramArray; + } + + public static PresetResponseDto createPresetResponse(Preset preset) { + return PresetResponseDto.builder() + .presetId(preset.getPresetId()) + .title(preset.getTitle()) + .blockWebsiteArray( + preset.getBlockWebsiteArray() + .stream() + .sorted() + .toList() + ) + .blockProgramArray( + preset.getBlockProgramArray() + .stream() + .sorted() + .toList() + ) + .build(); + } + + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetWrapperResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetWrapperResponseDto.java index 2b00d277..598ac8ce 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetWrapperResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/PresetWrapperResponseDto.java @@ -1,3 +1,41 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c3f380bd23cebe1fca4dc0e810390f8aa13a32fc304dc2578c76eceba41d4a1 -size 1264 +package com.dinnertime.peaktime.domain.preset.service.dto.response; + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +public class PresetWrapperResponseDto { + + private List presetList; + + @Builder + private PresetWrapperResponseDto(List presetList){ + this.presetList = presetList; + } + + public static PresetWrapperResponseDto buildPresetResponseDto(List presets) { + List responseDto = presets.stream() + .map(PresetResponseDto::createPresetResponse) + .toList(); + + return PresetWrapperResponseDto.builder() + .presetList(responseDto) + .build(); + } + + public static PresetWrapperResponseDto buildPresetResponseDto(Preset preset) { + List responseDto = new ArrayList<>(); + PresetResponseDto presetResponse = PresetResponseDto.createPresetResponse(preset); + responseDto.add(presetResponse); + + return PresetWrapperResponseDto.builder() + .presetList(responseDto) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/SaveUrlPresetResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/SaveUrlPresetResponseDto.java index 18960461..514210e4 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/SaveUrlPresetResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/preset/service/dto/response/SaveUrlPresetResponseDto.java @@ -1,3 +1,34 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d8cd4c013f284745966fb23423e4e54941d6d5e2d4a3691268570e56ecd16b3 -size 1079 +package com.dinnertime.peaktime.domain.preset.service.dto.response; + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SaveUrlPresetResponseDto { + private Long presetId; + private List blockWebsiteArray; + + @Builder + private SaveUrlPresetResponseDto(Long presetId, List blockWebsiteArray) { + this.presetId = presetId; + this.blockWebsiteArray = blockWebsiteArray; + } + + public static SaveUrlPresetResponseDto createSaveUrlPresetResponseDto(Preset preset) { + return SaveUrlPresetResponseDto.builder() + .presetId(preset.getPresetId()) + .blockWebsiteArray( + preset.getBlockWebsiteArray() + .stream() + .sorted() + .toList() + ) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/controller/ScheduleController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/controller/ScheduleController.java index 107ee8ad..9a02ff07 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/controller/ScheduleController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/controller/ScheduleController.java @@ -1,3 +1,51 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c3b31f7db8d7ca0b51a7b5a4145a8f2518f498898aa98c93fc938e13ff8e788b -size 2564 +package com.dinnertime.peaktime.domain.schedule.controller; + +import com.dinnertime.peaktime.domain.schedule.service.ScheduleService; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/schedules") +public class ScheduleController { + private final ScheduleService scheduleService; + + //타입을 TEXT_EVENT_STREAM_VALUE 명시해야 서버가 클라어언트에 메시지 스트림을 전송한다는 것을 명시하여 연결 유지 가능 + //lastEventId sse 연결이 끊어졌을 경우 마지막 이벤트 아이디 + @Operation(summary = "SSE 구독", description = "SSE 구독하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "구독하는데 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "구독하는데 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping(value = "", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribe( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestHeader(value = "LAST-EVENT-ID", required = false, defaultValue = "") String lastEventId + ) { + log.info("구독"); + //구독 하기 + return scheduleService.subScribe(userPrincipal.getUserId(), lastEventId); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/entity/Schedule.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/entity/Schedule.java index e154cf10..3fafe25d 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/entity/Schedule.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/entity/Schedule.java @@ -1,3 +1,55 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a393d518bae209ad09336651dc02e7c04b6e053eeff4701e570ec6dabea5fb5f -size 1645 +package com.dinnertime.peaktime.domain.schedule.entity; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "schedules") +public class Schedule { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "schedule_id") + private Long scheduleId; + + //6이 월 0이 일 + //요일을 나타내는 컬럼 + @Column(name = "day_of_week", nullable = false) + private int dayOfWeek; + + //이벤트 발생 시간 -> 시작 시간 + @Column(name = "start_time", nullable = false) + private LocalTime startTime; + + @Column(name = "attention_time", nullable = false) + private int attentionTime; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "timer_id", nullable = false) + private Timer timer; + + @Builder + private Schedule(int dayOfWeek, LocalTime startTime, int attentionTime, Timer timer) { + this.dayOfWeek = dayOfWeek; + this.startTime = startTime; + this.attentionTime = attentionTime; + this.timer = timer; + } + + + public static Schedule createSchedule(int dayOfWeek, LocalTime startTime, int attentionTime, Timer timer) { + return Schedule.builder() + .dayOfWeek(dayOfWeek) + .startTime(startTime) + .attentionTime(attentionTime) + .timer(timer) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepository.java index 94c249ce..ef5adb23 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepository.java @@ -1,3 +1,23 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:32790c0547b7c28c5754966d966ec3f17ea2bcab52ee65529278b3cb9ce76403 -size 685 +package com.dinnertime.peaktime.domain.schedule.repository; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; + +public interface EmitterRepository { + SseEmitter save(String emitterId, SseEmitter emitter); + + void saveEventCache(String emitterId, Object event); + + //key값의 경우 groupId로 시작하므로 + Map findEmitterByGroupId(Long groupId); + + //key값의 경우 groupId로 시작하므로 + Map findEmitterCacheByGroupId(Long groupId); + + void deleteById(String emitterId); + + void deleteEmitterAllByGroupId(Long groupId); + + void deleteCacheAllByGroupId(Long groupId); +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepositoryImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepositoryImpl.java index 860cdfc3..9018f901 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepositoryImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/repository/EmitterRepositoryImpl.java @@ -1,3 +1,79 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fee8e6005b21cf4b591d7decc0b9afeb89e1e4a862fe3c4dad7f2fa703d684c8 -size 2983 +package com.dinnertime.peaktime.domain.schedule.repository; + +import org.springframework.stereotype.Repository; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +//사용자 정보만 저장 +@Repository +public class EmitterRepositoryImpl implements EmitterRepository { + //동시에 여러 사람이 접근해도 thread-safe한 ConcurrentHashMap + private final Map emitterList = new ConcurrentHashMap<>(); + //사용자에게 주지 못한 이벤트들 저장하여 후에 이벤트를 보내주는 맵 + private final Map eventCache = new ConcurrentHashMap<>(); + + @Override + public SseEmitter save(String emitterId, SseEmitter emitter) { + //각 사용자 sse저장 + emitterList.put(emitterId, emitter); + return emitter; + } + + @Override + public void saveEventCache(String emitterId, Object event) { + //이벤트 캐시에 저장 + eventCache.put(emitterId, event); + } + + //key값의 경우 groupId로 시작하므로 + @Override + public Map findEmitterByGroupId(Long groupId) { + //entrySet -> 모든 키, 값을 set객체로 생성 + return emitterList.entrySet().stream() + //groupId로 시작하는 것들 모두 가져와서 map으로 만들어서 반환 + .filter(entry -> entry.getKey().startsWith(groupId.toString())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + //key값의 경우 groupId로 시작하므로 + @Override + public Map findEmitterCacheByGroupId(Long groupId) { + //entrySet -> 모든 키, 값을 set객체로 생성 + return emitterList.entrySet().stream() + //groupId로 시작하는 것들 모두 가져와서 map으로 만들어서 반환 + .filter(entry -> entry.getKey().startsWith(groupId.toString())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public void deleteById(String emitterId) { + emitterList.remove(emitterId); + } + + @Override + public void deleteEmitterAllByGroupId(Long groupId) { + emitterList.forEach( + (key, emitter) -> { + //key가 groupId로 시작하는 얘들 전부 삭제 + if(key.startsWith(groupId.toString())) { + emitterList.remove(key); + } + } + ); + } + + @Override + public void deleteCacheAllByGroupId(Long groupId) { + eventCache.forEach( + (key, value) -> { + //key가 groupId로 시작하는 얘들 전부 삭제 + if(key.startsWith(groupId.toString())) { + eventCache.remove(key); + } + } + ); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/ScheduleService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/ScheduleService.java index 510f7bc8..b2a9d891 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/ScheduleService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/ScheduleService.java @@ -1,3 +1,167 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:93317308a149004be06790e37b0f3f6eb2286e8ed6648b743b7c61444feda112 -size 6990 +package com.dinnertime.peaktime.domain.schedule.service; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import com.dinnertime.peaktime.domain.schedule.entity.Schedule; +import com.dinnertime.peaktime.domain.schedule.repository.EmitterRepository; +import com.dinnertime.peaktime.domain.schedule.repository.ScheduleRepository; +import com.dinnertime.peaktime.domain.schedule.service.dto.RedisSchedule; +import com.dinnertime.peaktime.domain.schedule.service.dto.response.SendTimerResponseDto; +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import com.dinnertime.peaktime.domain.timer.service.dto.request.TimerCreateRequestDto; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.RedisService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ScheduleService { + + private final ScheduleRepository scheduleRepository; + private final GroupRepository groupRepository; + private final UserGroupRepository userGroupRepository; + private final EmitterRepository emitterRepository; + private final RedisService redisService; + + //연결 지속시간 한시간 + private static final long DEFAULT_TIMEOUT = 60L * 1000 * 60; + private static final int DAY = 7; + private static final int DAY_MINUTE = 1440; + + public SseEmitter subScribe(Long userId, String lastEventId) { + //그룹 가져오기 + UserGroup userGroup = userGroupRepository.findByUser_UserId(userId).orElseThrow( + () -> new CustomException(ErrorCode.GROUP_NOT_FOUND) + ); + + Long groupId = userGroup.getGroup().getGroupId(); + + //고유한 아이디 생성 + //뒤에 시간이 있는 이유는 연결끊겼을때 전송해야하는 메시지를 보내기 위해 사용 + String emitterId = groupId+"_"+userId+"_"+System.currentTimeMillis(); + + //저장 + SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT)); + + //시간 초과나 비동기 요청 안되면 삭제 + emitter.onCompletion(()->emitterRepository.deleteById(emitterId)); + emitter.onTimeout(() -> emitterRepository.deleteById(emitterId)); + + //최초 연결시 메시지를 안보낼 경우 503에러 발생 하므로 데미 데이터 전송 + sendToClient(emitter, emitterId, "start"); + + //lastEventId 이게 있으면 연결이 종료 되었다는 뜻 연결 지속시간 + //남아 있는 모든 데이터를 전송 + if(!lastEventId.isEmpty()) { + Map events = emitterRepository.findEmitterCacheByGroupId(groupId); + events.entrySet().stream() + //저장된 key값 비교를 통해 유실된 데이터만 재전송 할 수 있음 + .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) + .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue())); + } + + return emitter; + } + + public void sendToClient(SseEmitter emitter, String emitterId, Object data) { + try { + //데이터를 보냄 + emitter.send(SseEmitter.event() + .id(emitterId) + .data(data)); + } catch (IOException e) { + emitterRepository.deleteById(emitterId); + emitter.completeWithError(e); + } + } + + @Transactional(readOnly = true) + public void send(Long groupId, int attentionTime) { + Map sseEmitterList = emitterRepository.findEmitterByGroupId(groupId); + + Group group = groupRepository.findByGroupIdAndIsDeleteFalse(groupId).orElseThrow( + () -> new CustomException(ErrorCode.GROUP_NOT_FOUND) + ); + + Preset preset = group.getPreset(); + SendTimerResponseDto responseDto = SendTimerResponseDto.createSendTimerResponseDto(attentionTime, preset); + + sseEmitterList.forEach( + (key, emitter) -> CompletableFuture.runAsync(() -> { + emitterRepository.saveEventCache(key, responseDto); + sendToClient(emitter, key, responseDto); + }) + ); + } + + + @Transactional + public List createSchedule(TimerCreateRequestDto requestDto, Timer timer) { + int repeatDay = requestDto.getRepeatDay(); + int attentionTime = requestDto.getAttentionTime(); + LocalTime startTime = requestDto.getStartTime().toLocalTime(); + + List scheduleList = new ArrayList<>(); + + //6이 월요일 0이 일요일 + for(int day=0; day getNowDaySchedule() { + int dayOfWeek = DAY - LocalDate.now().getDayOfWeek().getValue(); + + List scheduleList = scheduleRepository.findAllByDayOfWeek(dayOfWeek); + + return scheduleList.stream() + .map(RedisSchedule::createRedisSchedule) + .toList(); + } + + @Transactional + public void deleteSchedule(Timer timer) { + scheduleRepository.deleteAllByTimer_TimerId(timer.getTimerId()); + } + + @Transactional + public void deleteSchedule(Long timerId) { + scheduleRepository.deleteAllByTimer_TimerId(timerId); + } + + public void saveTodayScheduleToRedis(List scheduleList, int repeatDay, LocalDateTime startTime) { + int todayDayOfWeek = DAY - LocalDateTime.now().getDayOfWeek().getValue(); + LocalTime startLocalTime = startTime.toLocalTime(); + + //오늘날짜에 있으면 + scheduleList.stream() + .filter(s -> s.getDayOfWeek() == todayDayOfWeek && (repeatDay & (1 << todayDayOfWeek)) != 0 && startLocalTime.isAfter(LocalTime.now())) + .findFirst() + .ifPresent(redisService::addSchedule); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/SchedulerService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/SchedulerService.java index ae9eb139..e8abbd84 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/SchedulerService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/SchedulerService.java @@ -1,3 +1,65 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba991ea34fc64354f1108e7cc25bf89f5f605de6193af6e1081c7cf0277c60f9 -size 2155 +package com.dinnertime.peaktime.domain.schedule.service; + +import com.dinnertime.peaktime.domain.hiking.repository.HikingRepository; +import com.dinnertime.peaktime.domain.schedule.entity.Schedule; +import com.dinnertime.peaktime.domain.schedule.service.dto.RedisSchedule; +import com.dinnertime.peaktime.global.util.RedisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SchedulerService { + + private final ScheduleService scheduleService; + private final RedisService redisService; + private final HikingRepository hikingRepository; + + //매일 0시에 실행 + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + public void addScheduling() { + //오늘 날짜 기준으로 가져오기 + List scheduleList = scheduleService.getNowDaySchedule(); + + log.info(scheduleList.get(0).toString()); + + //저장 + redisService.addFirstSchedule(scheduleList); + } + + //1분마다 실행 + @Scheduled(cron = "0 0/1 * * * *") + public void send() { + LocalDateTime now = LocalDateTime.now(); + int day = 7 - now.getDayOfWeek().getValue(); + int hour = now.getHour(); + int minute = now.getMinute(); + + int start = day * 1440 + hour * 60 + minute; + + List timerList = redisService.findTimerByStart(start); + + if(timerList == null || timerList.isEmpty()) { + return; + } + + log.info("exist timer: "+ now); + + timerList.forEach(timer -> { + String[] split = timer.split("-"); + // 집중 시간 계산 + int attentionTime = Integer.parseInt(split[1]) - Integer.parseInt(split[0]); + Long groupId = Long.parseLong(split[2]); + // `send` 메서드로 groupId와 attentionTime 전송 + scheduleService.send(groupId, attentionTime); + }); + + } +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/RedisSchedule.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/RedisSchedule.java index 5fcbe7fb..8ea037af 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/RedisSchedule.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/RedisSchedule.java @@ -1,3 +1,56 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7956059c696d8d4529660f52c48c9a643bfd8996723975db1b25d5cc30bb0e5 -size 1916 +package com.dinnertime.peaktime.domain.schedule.service.dto; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.schedule.entity.Schedule; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import jakarta.persistence.Column; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RedisSchedule implements Serializable { + + //6이 월 0이 일 + //요일을 나타내는 컬럼 + private int dayOfWeek; + + //이벤트 발생 시간 -> 시작 시간 + private String startTime; + + private int attentionTime; + + private Long groupId; + + private Long timerId; + + @Builder + private RedisSchedule(int dayOfWeek, String startTime, int attentionTime, Long groupId, Long timerId) { + this.dayOfWeek = dayOfWeek; + this.startTime = startTime; + this.attentionTime = attentionTime; + this.groupId = groupId; + this.timerId = timerId; + } + + public static RedisSchedule createRedisSchedule(Schedule schedule) { + return RedisSchedule.builder() + .dayOfWeek(schedule.getDayOfWeek()) + .startTime(String.valueOf(schedule.getStartTime())) + .attentionTime(schedule.getAttentionTime()) + .groupId(schedule.getTimer().getGroup().getGroupId()) + .timerId(schedule.getTimer().getTimerId()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/response/SendTimerResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/response/SendTimerResponseDto.java index cba43660..3003e411 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/response/SendTimerResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/schedule/service/dto/response/SendTimerResponseDto.java @@ -1,3 +1,40 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b207cd804571170cd874b515047734a3228568759f6b3c081694cf9e33ca62e3 -size 1263 +package com.dinnertime.peaktime.domain.schedule.service.dto.response; + +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SendTimerResponseDto { + + private Integer attentionTime; + + private Long presetId; + + private List blockWebsiteArray; + + private List blockProgramArray; + + + @Builder + private SendTimerResponseDto(Integer attentionTime, Long presetId, List blockWebsiteArray, List blockProgramArray) { + this.attentionTime = attentionTime; + this.presetId = presetId; + this.blockWebsiteArray = blockWebsiteArray; + this.blockProgramArray = blockProgramArray; + } + + public static SendTimerResponseDto createSendTimerResponseDto(Integer attentionTime, Preset preset) { + return SendTimerResponseDto.builder() + .attentionTime(attentionTime) + .presetId(preset.getPresetId()) + .blockWebsiteArray(preset.getBlockWebsiteArray()) + .blockProgramArray(preset.getBlockProgramArray()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/entity/Statistic.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/entity/Statistic.java index a3edd99e..4298204e 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/entity/Statistic.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/entity/Statistic.java @@ -1,3 +1,84 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8f689a80535c91ddebeefa96804618ec537f7e1f33697ef4c144acc06a709eb -size 2812 +package com.dinnertime.peaktime.domain.statistic.entity; + +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingStatisticQueryDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "statistics") +@Getter +public class Statistic { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "statistic_id") + private Long statisticId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "total_hiking_time") + private Integer totalHikingTime; + + @Column(name = "total_hiking_count") + private Long totalHikingCount; + + @Column(name = "total_success_count") + private Integer totalSuccessCount; + + @Column(name = "total_block_count") + private Long totalBlockCount; + + @Type(JsonBinaryType.class) + @Column(name="start_time_array", columnDefinition = "jsonb") + private List startTimeArray; + + @Type(JsonBinaryType.class) + @Column(name = "most_site_array", columnDefinition = "jsonb") + private List mostSiteArray; + + @Type(JsonBinaryType.class) + @Column(name = "most_program_array",columnDefinition = "jsonb") + private List mostProgramArray; + + @Builder + private Statistic(User user) { + this.user = user; + this.totalHikingTime = 0; + this.totalHikingCount = 0L; + this.totalSuccessCount = 0; + this.totalBlockCount = 0L; + this.startTimeArray = new ArrayList<>(); + this.mostSiteArray = new ArrayList<>(); + this.mostProgramArray = new ArrayList<>(); + } + + //사용자가 처음 회원가입할 때 + public static Statistic createFirstStatistic(User user) { + return Statistic.builder() + .user(user) + .build(); + } + + //업데이트 용 + public void updateStatistic(HikingStatisticQueryDto hikingStatistic, Long totalBlockedCount, List siteList, List programList, List startTimeList) { + this.totalHikingTime = hikingStatistic.getTotalHikingTime(); + this.totalHikingCount = hikingStatistic.getTotalHikingCount(); + this.totalSuccessCount = hikingStatistic.getTotalHikingSuccessCount(); + this.totalBlockCount = totalBlockedCount; + this.mostSiteArray = siteList; + this.mostProgramArray = programList; + this.startTimeArray = startTimeList; + } + +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/repository/StatisticRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/repository/StatisticRepository.java index f262e16c..17429e8e 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/repository/StatisticRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/repository/StatisticRepository.java @@ -1,3 +1,18 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:885cfef6a00f2d986c56693311259617f9551363f76a1173f1e6adfc15714c59 -size 633 +package com.dinnertime.peaktime.domain.statistic.repository; + +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import com.dinnertime.peaktime.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface StatisticRepository extends JpaRepository { + //이미 존재하는 유저일 경우 statistic없어서 optional처리 + Optional findByUser_UserId(Long userId); + + List findAllByUserIn(List userList); + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/service/StatisticService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/service/StatisticService.java index 3325ca31..b5f93c48 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/service/StatisticService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/statistic/service/StatisticService.java @@ -1,3 +1,72 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d89b02add047b42ef099515f2c922d8adc1af9fe36bbb9703f99ddbcd1b741ca -size 3176 +package com.dinnertime.peaktime.domain.statistic.service; + +import com.dinnertime.peaktime.domain.content.repository.ContentRepository; +import com.dinnertime.peaktime.domain.hiking.repository.HikingRepository; +import com.dinnertime.peaktime.domain.hiking.service.dto.query.HikingStatisticQueryDto; +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import com.dinnertime.peaktime.domain.statistic.entity.StatisticContent; +import com.dinnertime.peaktime.domain.statistic.repository.StatisticRepository; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class StatisticService { + + private final HikingRepository hikingRepository; + private final ContentRepository contentRepository; + private final UserRepository userRepository; + private final StatisticRepository statisticRepository; + + //매일 1시에 실행 + @Scheduled(cron = "0 0 1 * * *", zone = "Asia/Seoul") +// @Scheduled(cron = "0 0/1 * * * *", zone = "Asia/Seoul") + public void updateStatistics() { + + List findUserList = userRepository.findAllByIsDeleteIsFalse(); + + for (User findUser : findUserList) { + Long findUserId = findUser.getUserId(); + + Optional optionalStatistic = statisticRepository.findByUser_UserId(findUserId); + if(optionalStatistic.isEmpty()) { + Statistic statistic = Statistic.createFirstStatistic(findUser); + statisticRepository.save(statistic); + } + + HikingStatisticQueryDto hikingStatistic = hikingRepository.getHikingStatistic(findUserId); + + if (hikingStatistic == null) { + continue; + } + //전체 차단 접근 횟수 + Long totalBlockedCount = hikingRepository.getTotalBlockedCount(findUserId); + //사이트 리스트 조회 + List siteList = contentRepository.getTopUsingInfoListByUserId("site", findUserId); + //프로그램 리스트 조회 + List programList = contentRepository.getTopUsingInfoListByUserId("program", findUserId); + //시작 시간 리스트 조회 + List startDateTimeList = hikingRepository.getStartTimeListByUserId(findUserId); + + List startTimeList = startDateTimeList.stream() + .map(localDateTime -> localDateTime.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm"))) + .toList(); + + if(optionalStatistic.isPresent()) { + Statistic statistic = optionalStatistic.get(); + + statistic.updateStatistic(hikingStatistic, totalBlockedCount, siteList, programList, startTimeList); + + statisticRepository.save(statistic); + } + } + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/controller/SummaryController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/controller/SummaryController.java index 21e27278..035b87b3 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/controller/SummaryController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/controller/SummaryController.java @@ -1,3 +1,123 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07535e3d699a9f7e1625ccb177542ca1e88da959fac0b3d173971335da0d7119 -size 6052 +package com.dinnertime.peaktime.domain.summary.controller; + +import com.dinnertime.peaktime.domain.summary.service.SummaryFacade; +import com.dinnertime.peaktime.domain.summary.service.dto.request.SaveSummaryRequestDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.CreateSummaryResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryDetailResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryWrapperResponseDto; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/summaries") +@RequiredArgsConstructor +public class SummaryController { + + private final SummaryFacade summaryFacade; + + // 요약 저장 + @Operation(summary = "gpt 요약 내용 저장하기", description = "요약내용 저장하기") + @ApiResponses(value ={ + @ApiResponse(responseCode = "200", description = "요청된 요약 내용 저장에 성공했습니다.", + content=@Content(schema = @Schema(implementation = CreateSummaryResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "요청된 요약 내용 저장에 실패했습니다.", + content=@Content(schema = @Schema(implementation = ResultDto.class)) + ), + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping() + public ResponseEntity createSummary( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @Valid @RequestBody SaveSummaryRequestDto requestDto) { + + log.info("createSummary 메서드가 호출되었습니다."); + log.info("요약 생성 : " + requestDto.toString()); + + CreateSummaryResponseDto responseDto = summaryFacade.createSummary(requestDto, userPrincipal.getUserId()); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "요약 생성에 성공했습니다.", responseDto)); + + } + + + // 요약 삭제 + @Operation(summary = "저장된 요약 삭제하기", description = "요약 내용 삭제하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요약 내용 삭제에 성공했습니다.", + content = @Content(schema= @Schema(implementation = ResultDto.class)) + ), + @ApiResponse(responseCode = "500", description = "요약 내용 삭제에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("/{summaryId}") + public ResponseEntity deleteSummary(@PathVariable Long summaryId) { + + log.info("deleteSummary 메서드가 호출되었습니다."); + + summaryFacade.deleteSummary(summaryId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"요약 내용 삭제에 성공했습니다.")); + } + + + // 요약 리스트 조회 + @Operation(summary = "요약 리스트 조회", description = "화면 진입 시 요약의 리스트를 확인함") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요약 리스트 조회에 성공했습니다.", + content = @Content(schema= @Schema(implementation = SummaryWrapperResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "요약 리스트 조회에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping + public ResponseEntity getSummaryTitles ( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestParam("page") int page) { + log.info("getSummaryTitles 메서드가 호출되었습니다."); + + SummaryWrapperResponseDto responseDto = summaryFacade.getSummaryList(userPrincipal.getUserId(), page); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"요약 리스트 조회에 성공했습니다.", responseDto)); + } + + + // 요약 디테일 조회 + @Operation(summary = "요약 상세 조회", description = "요약 리스트 내 타이틀 클릭시 발생하는 상세 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요약 상세 조회에 성공했습니다.", + content = @Content(schema= @Schema(implementation = SummaryDetailResponseDto.class)) + ), + @ApiResponse(responseCode = "500", description = "요약 상세 조회에 실패했습니다.", + content= @Content(schema= @Schema(implementation = ResultDto.class)) + ) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/{summaryId}") + public ResponseEntity getSummaryDetail (@PathVariable("summaryId") Long summaryId) { + log.info("getSummaryDetail 메서드가 호출되었습니다."); + + SummaryDetailResponseDto responseDto = summaryFacade.getSummaryDetail(summaryId); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(),"요약 상세 조회에 성공했습니다.", responseDto)); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/entity/Summary.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/entity/Summary.java index 9d9b6f21..4cbd9108 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/entity/Summary.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/entity/Summary.java @@ -1,3 +1,51 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5e63b76a02f363bef0c2c9bd809492256241685c518d9fc67f6045694f385278 -size 1495 +package com.dinnertime.peaktime.domain.summary.entity; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import com.dinnertime.peaktime.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name="summaries") +public class Summary { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="summary_id") + private Long summaryId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="user_id", nullable = false) + private User user; + + @Column(name="created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "content", columnDefinition = "TEXT", nullable = false) + private String content; + + @Column(name = "title", length = 20, nullable = false) + private String title; + + @Builder + private Summary(LocalDateTime createdAt, String content, String title, User user) { + this.createdAt = createdAt; + this.content = content; // gpt 내용이 담겨야 함 + this.title = title; + this.user = user; + } + + // 요약 내용 저장 + public static Summary createSummary(String GPTContent, String title, User user) { + return Summary.builder() + .createdAt(LocalDateTime.now()) + .content(GPTContent) // gpt 내용이 담겨야 함 + .title(title) + .user(user) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/repository/SummaryRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/repository/SummaryRepository.java index 9645c661..3680541f 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/repository/SummaryRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/repository/SummaryRepository.java @@ -1,3 +1,20 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6aa2e6406461857a198bead6dc29cc632d09f227ae8a1ae236ba40551ab388c0 -size 722 +package com.dinnertime.peaktime.domain.summary.repository; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import com.dinnertime.peaktime.domain.summary.entity.Summary; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SummaryRepository extends JpaRepository { + + // 요약 단독 삭제 시 사용하기 위함 + Optional findBySummaryId(Long summaryId); + + // 메모리스트 조회시 사용 + Page findAllByUser_UserId(Long userId, Pageable pageable); +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryFacade.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryFacade.java index 0899ab9e..8c35138b 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryFacade.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryFacade.java @@ -1,3 +1,57 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:656211059f906796226b36c283b9914a3292d7cffd082717ee2c3bd2789d11c3 -size 2144 +package com.dinnertime.peaktime.domain.summary.service; + +import com.dinnertime.peaktime.domain.memo.service.dto.response.MemoWrapperResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.request.SaveSummaryRequestDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.CreateSummaryResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryDetailResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryWrapperResponseDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.service.UserService; +import com.dinnertime.peaktime.global.util.RedisService; +import com.dinnertime.peaktime.global.util.chatgpt.ChatGPTService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class SummaryFacade { + + private final SummaryService summaryService; + private final ChatGPTService chatGPTService; + private final UserService userService; + private final RedisService redisService; + + //요약 생성 + public CreateSummaryResponseDto createSummary(SaveSummaryRequestDto requestDto, Long userId){ + + //유저 찾기 + User user = userService.getUser(userId); + + //gpt 생성 + String GPTContent = chatGPTService.getGPTResult(requestDto, userId); + + //요약 내용 저장 + summaryService.createSummary(requestDto, GPTContent, user); + + Integer gptCount = redisService.getGPTcount(userId); + + return CreateSummaryResponseDto.createSummaryResponseDto(gptCount); + } + + //요약 삭제 + public void deleteSummary(Long summaryId){ + summaryService.deleteSummary(summaryId); + } + + //요약 리스트 조회 + public SummaryWrapperResponseDto getSummaryList(Long userId, int page) { + return summaryService.getSummaryList(userId, page); + } + + //요약 상세 조회 + public SummaryDetailResponseDto getSummaryDetail(Long summaryId) { + return summaryService.getSummaryDetail(summaryId); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryService.java index bf8f7219..c88a8ac3 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/SummaryService.java @@ -1,3 +1,75 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31ec3266bfd2b5a96aec0dbaffd41f671336235b88b01ac3808ffc7ad2b27aad -size 2987 +package com.dinnertime.peaktime.domain.summary.service; + +import com.dinnertime.peaktime.domain.memo.entity.Memo; +import com.dinnertime.peaktime.domain.memo.repository.MemoRepository; +import com.dinnertime.peaktime.domain.summary.entity.Summary; +import com.dinnertime.peaktime.domain.summary.repository.SummaryRepository; +import com.dinnertime.peaktime.domain.summary.service.dto.request.SaveSummaryRequestDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryDetailResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryResponseDto; +import com.dinnertime.peaktime.domain.summary.service.dto.response.SummaryWrapperResponseDto; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Slf4j +@RequiredArgsConstructor +@Service +public class SummaryService { + + // 요약 저장, 삭제 구현 + private final SummaryRepository summaryRepository; + private final MemoRepository memoRepository; + + // 요약 정보 저장 및 업데이트 + @Transactional + public void createSummary(SaveSummaryRequestDto requestDto, String GPTContent, User user) { + // insert + Summary createdSummary = Summary.createSummary(GPTContent, requestDto.getTitle(), user); + summaryRepository.save(createdSummary); + } + + + @Transactional + public void deleteSummary(Long summaryId) { + + Summary summary = summaryRepository.findBySummaryId(summaryId) + .orElseThrow(() -> new CustomException(ErrorCode.SUMMARY_NOT_FOUND)); + + summaryRepository.delete(summary); + } + + @Transactional(readOnly = true) + public SummaryWrapperResponseDto getSummaryList(Long userId, int page) { + + //10개씩 조회 + Pageable pageable = PageRequest.of(page, 10); + + Page pageSummaryList = summaryRepository.findAllByUser_UserId(userId, pageable); + + List summaryList = pageSummaryList.stream() + .map(SummaryResponseDto::createSummaryResponseDto) + .toList(); + + return SummaryWrapperResponseDto.createMemoWrapperResponseDto(summaryList, pageSummaryList.isLast()); + } + + @Transactional(readOnly = true) + public SummaryDetailResponseDto getSummaryDetail(Long summaryId) { + Summary summary = summaryRepository.findBySummaryId(summaryId).orElseThrow( + () -> new CustomException(ErrorCode.SUMMARY_NOT_FOUND) + ); + + return SummaryDetailResponseDto.createSummaryDetailResponse(summary); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/request/SaveSummaryRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/request/SaveSummaryRequestDto.java index 619bacf1..73a126a6 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/request/SaveSummaryRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/request/SaveSummaryRequestDto.java @@ -1,3 +1,23 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5116f86cc27d13184ac124a7cd95e2ab20341de14eb33465b4afd22a75954487 -size 737 +package com.dinnertime.peaktime.domain.summary.service.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@Getter +@NoArgsConstructor(access= AccessLevel.PROTECTED) +public class SaveSummaryRequestDto { + + @NotNull + @Size(min = 2, max = 15, message = "제목은 2글자 이상 15글자 이하로 입력해주세요.") + private String title; + + @NotNull(message = "내용을 입력해주세요.") + private String content; // 질의한 내용 + + private String[] keywords; // 추가 키워드 +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/CreateSummaryResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/CreateSummaryResponseDto.java index 34c7ccc4..aa049714 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/CreateSummaryResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/CreateSummaryResponseDto.java @@ -1,3 +1,23 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d714d95852e4e3312c9428215c7b0afd84813a4d01afd56abf89297b1ebdb682 -size 681 +package com.dinnertime.peaktime.domain.summary.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CreateSummaryResponseDto { + private Integer summaryCount; // 하루 gpt 사용 횟수 + + @Builder + private CreateSummaryResponseDto(Integer summaryCount) { + this.summaryCount = summaryCount; + } + + public static CreateSummaryResponseDto createSummaryResponseDto(Integer summaryCount) { + return CreateSummaryResponseDto.builder() + .summaryCount(summaryCount) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryDetailResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryDetailResponseDto.java index 1e0e248d..a5fccd8b 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryDetailResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryDetailResponseDto.java @@ -1,3 +1,38 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:712820b8def62e53540ce76f14d2a9b14c444620048a98c30c72177cdcab7265 -size 1170 +package com.dinnertime.peaktime.domain.summary.service.dto.response; + +import com.dinnertime.peaktime.domain.summary.entity.Summary; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SummaryDetailResponseDto { + + private Long summaryId; + private String title; + private String content; + private LocalDateTime createdAt; + + // 요약 정보가 담긴 responseDto 작성 + @Builder + private SummaryDetailResponseDto(Long summaryId, String content, LocalDateTime createdAt, String title) { + this.summaryId = summaryId; + this.content = content; + this.createdAt = createdAt; + this.title = title; + } + + public static SummaryDetailResponseDto createSummaryDetailResponse(Summary summary) { + return SummaryDetailResponseDto.builder() + .summaryId(summary.getSummaryId()) + .content(summary.getContent()) + .createdAt(summary.getCreatedAt()) + .title(summary.getTitle()) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryResponseDto.java index 837fdc70..18ff2ba3 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryResponseDto.java @@ -1,3 +1,34 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9108221626be06ede8317e846c1351aab713706f5186ad7a34f176b392835f4d -size 1069 +package com.dinnertime.peaktime.domain.summary.service.dto.response; + +import com.dinnertime.peaktime.domain.summary.entity.Summary; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SummaryResponseDto { + // 요약 리스트 responseDto 작성 + private Long summaryId; + private String title; + private LocalDateTime createdAt; + + @Builder + private SummaryResponseDto(Long summaryId, String title, LocalDateTime createdAt) { + this.summaryId = summaryId; + this.title = title; + this.createdAt = createdAt; + } + + // 메모리스트 (내용 제외) responseDto 작성 + public static SummaryResponseDto createSummaryResponseDto(Summary summary) { + return SummaryResponseDto.builder() + .summaryId(summary.getSummaryId()) + .title(summary.getTitle()) + .createdAt(summary.getCreatedAt()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryWrapperResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryWrapperResponseDto.java index 218d9969..28d5654f 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryWrapperResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/summary/service/dto/response/SummaryWrapperResponseDto.java @@ -1,3 +1,28 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9594b6032193cf86b820540297c7d4b6d9a31453865a0295dc8ecad2c3210db9 -size 951 +package com.dinnertime.peaktime.domain.summary.service.dto.response; + +import com.dinnertime.peaktime.domain.memo.service.dto.response.MemoWrapperResponseDto; +import com.dinnertime.peaktime.domain.summary.entity.Summary; +import lombok.*; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +public class SummaryWrapperResponseDto { + private List summaryList; + private Boolean isLastPage; + + @Builder + private SummaryWrapperResponseDto(List summaryList, Boolean isLastPage) { + this.summaryList = summaryList; + this.isLastPage = isLastPage; + } + + public static SummaryWrapperResponseDto createMemoWrapperResponseDto(List summaryList, Boolean isLastPage) { + return SummaryWrapperResponseDto.builder() + .summaryList(summaryList) + .isLastPage(isLastPage) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/controller/TimerController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/controller/TimerController.java index 47e87558..2262a076 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/controller/TimerController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/controller/TimerController.java @@ -1,3 +1,58 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5af4fd1a4dce20b084d88d6160591b441001f4bf224396f4124c6fc2622308fc -size 3331 +package com.dinnertime.peaktime.domain.timer.controller; + +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupDetailResponseDto; +import com.dinnertime.peaktime.domain.timer.service.TimerService; +import com.dinnertime.peaktime.domain.timer.service.dto.request.TimerCreateRequestDto; +import com.dinnertime.peaktime.domain.timer.service.facade.TimerFacade; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/timers") +@RequiredArgsConstructor +public class TimerController { + + private final TimerFacade timerFacade; + + @Operation(summary = "그룹 타이머 생성", description = "그룹의 타이머 목록과 비교해서 겹치는 시간이 존재하지 않을 때 타이머를 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "타이머 생성을 성공했습니다.", + content = @Content(schema = @Schema(implementation = GroupDetailResponseDto.class))), + @ApiResponse(responseCode = "409", description = "선택한 시간 범위가 다른 예약과 겹칩니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "타이머를 생성하는 데 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("") + public ResponseEntity postTimer(@RequestBody @Valid TimerCreateRequestDto requestDto) { + GroupDetailResponseDto responseDto = timerFacade.createTimer(requestDto); + + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "타이머 생성을 성공했습니다.", responseDto)); + } + + @Operation(summary = "그룹 타이머 삭제", description = "선택한 그룹 타이머를 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "타이머 삭제를 성공했습니다.", + content = @Content(schema = @Schema(implementation = GroupDetailResponseDto.class))), + @ApiResponse(responseCode = "500", description = "타이머 삭제를 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("/{timerId}") + public ResponseEntity deleteTimer(@PathVariable("timerId") Long timerId) { + GroupDetailResponseDto responseDto = timerFacade.deleteTimer(timerId); + return ResponseEntity.status(HttpStatus.OK).body(ResultDto.res(HttpStatus.OK.value(), "타이머 삭제를 성공했습니다.", responseDto)); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/entity/Timer.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/entity/Timer.java index bde46c5f..bebc6b97 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/entity/Timer.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/entity/Timer.java @@ -1,3 +1,59 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9bf2ef72dac6d2e0051bb5755a493ba500888de8c3aeda4a0fa8b0f15c2459bc -size 1723 +package com.dinnertime.peaktime.domain.timer.entity; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.timer.service.dto.request.TimerCreateRequestDto; +import jakarta.persistence.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "timers") +public class Timer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "timer_id") + private Long timerId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_id", nullable = false) + private Group group; + + @Column(name = "start_time", nullable = false) + private LocalDateTime startTime; + + @Column(name = "attention_time", nullable = false) +// @Min(30) + @Max(240) + private int attentionTime; + + @Column(name = "repeat_day", nullable = false) + @Min(1) + @Max(127) + private int repeatDay; + + @Builder + private Timer(Group group, LocalDateTime startTime, int attentionTime, int repeatDay) { + this.group = group; + this.startTime = startTime; + this.attentionTime = attentionTime; + this.repeatDay = repeatDay; + } + + public static Timer createTimer(Group group, TimerCreateRequestDto requestDto) { + return Timer.builder() + .group(group) + .startTime(requestDto.getStartTime()) + .attentionTime(requestDto.getAttentionTime()) + .repeatDay(requestDto.getRepeatDay()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/repository/TimerRepositoryImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/repository/TimerRepositoryImpl.java index a8a609ed..4d80fc3b 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/repository/TimerRepositoryImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/repository/TimerRepositoryImpl.java @@ -1,3 +1,62 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:34358ec9ccf7278010073469b1759ab4cf3739d92579b784dc11795f67ac2d62 -size 2880 +package com.dinnertime.peaktime.domain.timer.repository; + +import com.dinnertime.peaktime.domain.timer.entity.QTimer; +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class TimerRepositoryImpl implements TimerRepositoryCustom { + + private final EntityManager entityManager; + private final QTimer timer = QTimer.timer; + private final JPAQueryFactory queryFactory; + + @Override + public Boolean existsOverlappingTimers (Long groupId, LocalDateTime startTime, int attentionTime, int repeatDay) { + LocalDateTime requestEndTime = startTime.plusMinutes(attentionTime); + LocalTime localEndTime = LocalTime.of(requestEndTime.getHour(), requestEndTime.getMinute()); + LocalTime localStartTime = LocalTime.of(startTime.getHour(), startTime.getMinute()); + + int repeatDayNumber = (repeatDay == 0) ? (int) Math.pow(2, 7 - startTime.getDayOfWeek().getValue()) : repeatDay; + + String query = "SELECT COUNT(*) FROM timers t " + + "WHERE t.group_id = :groupId " + + "AND t.start_time::time <= :localEndTime " + + "AND (t.start_time::time + INTERVAL '1 minute' * t.attention_time) >= :localStartTime " + + "AND ( " + + " ((t.repeat_day & :repeatDayNumber) != 0) " + // 요일이 설정된 경우 + " OR " + + " (t.repeat_day = 0 AND (1 << (CASE WHEN EXTRACT(DOW FROM t.start_time) = 0 THEN 0 ELSE 7 - CAST(EXTRACT(DOW FROM t.start_time) AS INTEGER) END)) & :repeatDayNumber != 0) " + + " )"; + + Long count = (Long) entityManager.createNativeQuery(query) + .setParameter("groupId", groupId) + .setParameter("localEndTime", localEndTime) + .setParameter("localStartTime", localStartTime) + .setParameter("repeatDayNumber", repeatDayNumber) + .getSingleResult(); + + return count > 0; // 중복된 시간대의 타이머가 존재하면 true 반환 + } + + @Override + public List findByGroup_GroupId(Long groupId) { + return queryFactory.selectFrom(timer) + .where(timer.group.groupId.eq(groupId)) + .orderBy( + Expressions.stringTemplate("TO_CHAR({0}, 'HH24:MI:SS')", timer.startTime).asc(), +// Expressions.dateTemplate(LocalTime.class, "DATE_FORMAT({0}, '%h-%m-%s')",timer.startTime).asc() + timer.repeatDay.desc() + ) + .fetch(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/TimerService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/TimerService.java index f7ed19bc..870ab2b0 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/TimerService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/TimerService.java @@ -1,3 +1,87 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37d56eb6a15fcf20b5b4b67136da9fc7a4709ab94d10d9a39ef95dfe8adabb7e -size 3521 +package com.dinnertime.peaktime.domain.timer.service; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.group.service.GroupService; +import com.dinnertime.peaktime.domain.schedule.service.ScheduleService; +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupDetailResponseDto; +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import com.dinnertime.peaktime.domain.timer.repository.TimerRepository; +import com.dinnertime.peaktime.domain.timer.service.dto.request.TimerCreateRequestDto; +import com.dinnertime.peaktime.domain.timer.service.dto.response.TimerItemResponseDto; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.RedisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TimerService { + + private final TimerRepository timerRepository; + private final GroupRepository groupRepository; + + @Transactional + public Timer postTimer(TimerCreateRequestDto requestDto) { + Long groupId = requestDto.getGroupId(); + LocalDateTime startTime = requestDto.getStartTime(); + int attentionTime = requestDto.getAttentionTime(); + int repeatDay = requestDto.getRepeatDay(); + + // 그룹 정보 확인 + Group group = groupRepository.findByGroupIdAndIsDeleteFalse(requestDto.getGroupId()) + .orElseThrow(() -> new CustomException(ErrorCode.GROUP_NOT_FOUND)); + + // 중복되는 타이머가 있는지 확인 + if (timerRepository.existsOverlappingTimers(groupId, startTime, attentionTime, repeatDay)) { + throw new CustomException(ErrorCode.TIME_SLOT_OVERLAP); + } + + // 타이머 생성 및 저장 + Timer timer = Timer.createTimer(group, requestDto); + timerRepository.save(timer); + + return timer; + } + + @Transactional + public void deleteTimer(Long timerId) { + // is_repeat = false이고 repeat_day가 존재하지 않는 경우 + // 타이머 실행 완료 후 실행 + Timer timer = timerRepository.findByTimerId(timerId) + .orElseThrow(() -> new CustomException(ErrorCode.TIMER_NOT_FOUND)); + + timerRepository.delete(timer); + } + + @Transactional(readOnly = true) + public GroupDetailResponseDto getTimerByGroupId(Long groupId) { + + Group group = groupRepository.findByGroupIdAndIsDeleteFalse(groupId).orElseThrow( + () -> new CustomException(ErrorCode.GROUP_NOT_FOUND) + ); + + // 타이머 리스트 조회 + List timerList = timerRepository.findByGroup_GroupId(groupId) + .stream() + .map(TimerItemResponseDto::createTimeItemResponseDto) + .collect(Collectors.toList()); + + return GroupDetailResponseDto.createGroupDetailResponseDto(group, timerList); + } + + @Transactional(readOnly = true) + public Timer getTimer(Long timerId) { + return timerRepository.findByTimerId(timerId).orElseThrow( + () -> new CustomException(ErrorCode.TIMER_NOT_FOUND) + ); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/request/TimerCreateRequestDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/request/TimerCreateRequestDto.java index 5671f3ff..aa654c10 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/request/TimerCreateRequestDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/request/TimerCreateRequestDto.java @@ -1,3 +1,31 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d887e7f3eca3913e37240ea83b417522ac6ccb5c9f74e5379221bf11469cf02 -size 951 +package com.dinnertime.peaktime.domain.timer.service.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TimerCreateRequestDto { + + @NotNull + private Long groupId; + + @NotNull + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + +// @Min(value = 30, message = "집중시간은 최소 30분이상이어야 합니다.") + @Max(value = 240, message = "집중시간은 최대 240분이상이어야 합니다.") + private int attentionTime; + + @Min(value = 1, message = "반복 요일은 최소 한개이상이어야합니다.") + @Max(127) + private int repeatDay; +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/response/TimerItemResponseDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/response/TimerItemResponseDto.java index b92efa69..77b1d803 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/response/TimerItemResponseDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/dto/response/TimerItemResponseDto.java @@ -1,3 +1,36 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:036f42e18a64d4d011f639c9371b9c777cad65d8e8b1d488ee899ed003062f12 -size 1119 +package com.dinnertime.peaktime.domain.timer.service.dto.response; + +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TimerItemResponseDto { + + private Long timerId; + private LocalDateTime startTime; + private int attentionTime; + private int repeatDay; + + @Builder + private TimerItemResponseDto(Long timerId, LocalDateTime startTime, int attentionTime, int repeatDay) { + this.timerId = timerId; + this.startTime = startTime; + this.attentionTime = attentionTime; + this.repeatDay = repeatDay; + } + + public static TimerItemResponseDto createTimeItemResponseDto(Timer timer) { + return TimerItemResponseDto.builder() + .timerId(timer.getTimerId()) + .startTime(timer.getStartTime()) + .attentionTime(timer.getAttentionTime()) + .repeatDay(timer.getRepeatDay()) + .build(); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/facade/TimerFacade.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/facade/TimerFacade.java index c1a25086..286608e2 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/facade/TimerFacade.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/timer/service/facade/TimerFacade.java @@ -1,3 +1,71 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:27c74b77c9b513edc9ea94cd546c3d22b12fc50b069a0bb8884e75d11937b4b8 -size 2576 +package com.dinnertime.peaktime.domain.timer.service.facade; + +import com.dinnertime.peaktime.domain.group.service.dto.response.GroupDetailResponseDto; +import com.dinnertime.peaktime.domain.schedule.entity.Schedule; +import com.dinnertime.peaktime.domain.schedule.service.ScheduleService; +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import com.dinnertime.peaktime.domain.timer.service.TimerService; +import com.dinnertime.peaktime.domain.timer.service.dto.request.TimerCreateRequestDto; +import com.dinnertime.peaktime.global.util.RedisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TimerFacade { + private final TimerService timerService; + private final ScheduleService scheduleService; + private final RedisService redisService; + + @Transactional + public GroupDetailResponseDto createTimer(TimerCreateRequestDto requestDto) { + Long groupId = requestDto.getGroupId(); + LocalDateTime startTime = requestDto.getStartTime(); + int attentionTime = requestDto.getAttentionTime(); + int repeatDay = requestDto.getRepeatDay(); + int plusMinute = (startTime.getHour() * 60) + startTime.getMinute(); + + // 타이머와 스케줄 db 저장 + Timer timer = timerService.postTimer(requestDto); + List scheduleList = scheduleService.createSchedule(requestDto, timer); + + // Redis에 타이머 추가 + redisService.addTimerList(timer, repeatDay, plusMinute, attentionTime); + + //오늘날짜가 있으면 저장 + scheduleService.saveTodayScheduleToRedis(scheduleList, repeatDay, startTime); + + //조회 + return timerService.getTimerByGroupId(groupId); + } + + @Transactional + public GroupDetailResponseDto deleteTimer(Long timerId) { + + //타이머 찾기 + Timer timer = timerService.getTimer(timerId); + + //생성의 역순 + //레디스 스케쥴 삭제 -> 오늘 있을 경우 + redisService.deleteScheduleByTimer(timer); + + //레디스 타이머 삭제 + redisService.deleteTimerByTimer(timer); + + //스케쥴 삭제 + scheduleService.deleteSchedule(timerId); + + //타이머 삭제 db삭제 + timerService.deleteTimer(timerId); + + //반환 + return timerService.getTimerByGroupId(timer.getGroup().getGroupId()); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/controller/UserController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/controller/UserController.java index e99b2c83..ab9e4005 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/controller/UserController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/controller/UserController.java @@ -1,3 +1,208 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7762a9ba5b97eb84c5737adefb77f62abbddfb6407f46aa97bf32f7644b93909 -size 13922 +package com.dinnertime.peaktime.domain.user.controller; + +import com.dinnertime.peaktime.domain.user.service.UserService; +import com.dinnertime.peaktime.domain.user.service.dto.request.*; +import com.dinnertime.peaktime.domain.user.service.dto.response.GetProfileResponse; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + // 프로필 조회 + @Operation(summary = "프로필 조회", description = "프로필 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "프로필 조회 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = GetProfileResponse.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않은 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "프로필 조회 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("") + public ResponseEntity getProfile(@AuthenticationPrincipal UserPrincipal userPrincipal) { + GetProfileResponse response = userService.getProfile(userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "프로필 조회 요청에 성공하였습니다.", response)); + } + + // 닉네임 변경 + @Operation(summary = "닉네임 변경", description = "닉네임 변경하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "닉네임 수정 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "닉네임 형식이 올바르지 않습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않은 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "현재와 동일한 닉네임입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "닉네임 수정 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/nickname") + public ResponseEntity updateNickname(@RequestBody @Valid UpdateNicknameRequest updateNicknameRequest, @AuthenticationPrincipal UserPrincipal userPrincipal) { + userService.updateNickname(updateNicknameRequest, userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "닉네임 수정 요청에 성공하였습니다.")); + } + + // 회원탈퇴 + @Operation(summary = "회원탈퇴", description = "회원탈퇴하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원탈퇴 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "회원탈퇴 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @DeleteMapping("") + public ResponseEntity deleteUser(@AuthenticationPrincipal UserPrincipal userPrincipal) { + userService.deleteUser(userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "회원탈퇴 요청에 성공하였습니다.")); + } + + // 비밀번호 변경 + @Operation(summary = "비밀번호 변경", description = "비밀번호 변경하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "비밀번호 변경 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "현재와 동일한 비밀번호입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "비밀번호 변경 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/password") + public ResponseEntity updatePassword(@RequestBody @Valid UpdatePasswordRequest updatePasswordRequest, @AuthenticationPrincipal UserPrincipal userPrincipal) { + userService.updatePassword(updatePasswordRequest, userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "비밀번호 변경 요청에 성공하였습니다.")); + } + + // 이메일 변경 + @Operation(summary = "이메일 변경", description = "이메일 변경하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "이메일 변경 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "403", description = "이메일이 인증되지 않았습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않은 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "이미 존재하는 이메일입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "이메일 변경 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/email") + public ResponseEntity updateEmail(@RequestBody @Valid UpdateEmailRequest updateEmailRequest, @AuthenticationPrincipal UserPrincipal userPrincipal) { + userService.updateEmail(updateEmailRequest, userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "이메일 변경 요청에 성공하였습니다.")); + } + + // 비밀번호 검증 + @Operation(summary = "비밀번호 검증", description = "비밀번호 검증하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "비밀번호가 검증되었습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않은 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "비밀번호가 일치하지 않습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "비밀번호 검증 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/password") + public ResponseEntity checkPassword(@RequestBody @Valid CheckPasswordRequest checkPasswordRequest, @AuthenticationPrincipal UserPrincipal userPrincipal) { + userService.checkPassword(checkPasswordRequest, userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "비밀번호가 검증되었습니다.")); + } + + // 회원정보 관리 페이지 접근 권한 검사 + @Operation(summary = "회원정보 관리 페이지 접근 권한 검사", description = "회원정보 관리 페이지 접근 권한 검사하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원정보 관리 페이지 접근 권한 검사 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "403", description = "서브 계정은 회원정보 관리 페이지에 접근할 수 없습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 유저입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "비밀번호가 일치하지 않습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "회원정보 관리 페이지 접근 권한 검사 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/settings") + public ResponseEntity allowSettings(@RequestBody @Valid AllowSettingsRequest allowSettingsRequest, @AuthenticationPrincipal UserPrincipal userPrincipal) { + userService.allowSettings(allowSettingsRequest, userPrincipal); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "회원정보 관리 페이지 접근 권한 검사 요청에 성공하였습니다.")); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/entity/User.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/entity/User.java index 062bb692..71b15548 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/entity/User.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/entity/User.java @@ -1,3 +1,83 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1a8139df2943a91a62b41e790450cf2210a7b360f4dd13f18b2c2b017fd2b90e -size 2418 +package com.dinnertime.peaktime.domain.user.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="user_id") + private Long userId; // PK + + @Column(name="user_login_id", nullable = false, unique = true, length = 15) + private String userLoginId; // 유저 로그인 아이디 + + @Setter + @Column(name = "password", nullable = false, length = 64) + private String password; // 비밀번호 + + @Setter + @Column(name = "nickname", nullable = false, length = 20) + private String nickname; // 닉네임 + + @Setter + @Column(name = "email", unique = true, length = 64) + private String email; // 이메일 + + @Column(name = "is_root", nullable = false) + private Boolean isRoot; // 루트 유저 여부 + + @Column(name = "is_delete", nullable = false) + private Boolean isDelete; // 삭제 여부 + + @Builder + private User(String userLoginId, String password, String nickname, String email, Boolean isRoot, Boolean isDelete) { + this.userLoginId = userLoginId; + this.password = password; + this.nickname = nickname; + this.email = email; + this.isRoot = isRoot; + this.isDelete = isDelete; + } + + public static User createRootUser(String userLoginId, String password, String nickname, String email) { + return User.builder() + .userLoginId(userLoginId) + .password(password) + .nickname(nickname) + .email(email) + .isRoot(true) + .isDelete(false) // Default value + .build(); + } + + public static User createChildUser(String userLoginId, String password, String nickname) { + return User.builder() + .userLoginId(userLoginId) + .password(password) + .nickname(nickname) + .email(null) + .isRoot(false) + .isDelete(false) // Default value + .build(); + } + + public void updateNickname(String nickname){ + this.nickname = nickname; + } + + public void updatePassword(String password){ + this.password = password; + } + + public void deleteUser() { + this.email = null; + this.isDelete = true; + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepository.java index 297b580f..e7967da0 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepository.java @@ -1,3 +1,21 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:928fd2924403168395f9bbb31fca2638e3eb2abe64c580b2d30d742f097dc496 -size 815 +package com.dinnertime.peaktime.domain.user.repository; + +import com.dinnertime.peaktime.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository, UserRepositoryCustom { + Optional findByUserIdAndIsDeleteFalse(long userId); + Optional findByUserId(long userId); + Optional findByUserLoginId(String userLoginId); + Optional findByUserLoginIdAndIsDeleteFalse(String userLoginId); + Optional findByEmail(String email); + // 자식 계정 조회 + Optional findByUserIdAndIsDeleteFalseAndIsRootFalse(long userId); + + List findAllByIsDeleteIsFalse(); +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepositoryImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepositoryImpl.java index 077625fa..ecd43706 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepositoryImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/repository/UserRepositoryImpl.java @@ -1,3 +1,89 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4e0cc4078c386e1ee690df89e76bc0a32a2ffbdae12c41aa4b3ba7680d335ac -size 3619 +package com.dinnertime.peaktime.domain.user.repository; + +import com.dinnertime.peaktime.domain.group.entity.QGroup; +import com.dinnertime.peaktime.domain.user.entity.QUser; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.usergroup.entity.QUserGroup; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class UserRepositoryImpl implements UserRepositoryCustom { + + private final JPAQueryFactory queryFactory; + private final QUser user = QUser.user; + private final QGroup group = QGroup.group; + private final QUserGroup userGroup = QUserGroup.userGroup; + + @Override + public Long updateIsDeleteByGroupId(Long groupId) { + List childUserIdList = queryFactory.select(userGroup.user.userId) + .from(userGroup) + .where(userGroup.group.groupId.eq(groupId)) + .fetch(); + + return queryFactory + .update(user) + .set(user.isDelete, true) + .where(user.userId.in(childUserIdList)) + .execute(); + } + + @Override + public Long updateIsDeleteByRootUserId(Long rootUserId) { + //childUserList가 캐싱되어 성능 빨라짐 + List childUserIdList = queryFactory.select(userGroup.user.userId) + .from(userGroup) + .join(group) + .on(userGroup.group.groupId.eq(group.groupId)) + .where( + group.user.userId.eq(rootUserId) + .and(group.isDelete.isFalse()) + ).fetch(); + + return queryFactory.update(user) + .set(user.isDelete, true) + .where( + user.userId.in(childUserIdList) + .and(user.isDelete.isFalse())) + .execute(); + +// 밑의 방식은 유저를 업데이트할때마다 즉 한 행마다 서브쿼리 수행하여 성능 저하 발생 +// return queryFactory +// .update(user) +// .set(user.isDelete, true) +// .where(user.userId.in( +// JPAExpressions +// .select(userGroup.user.userId) +// .from(userGroup) +// .where(userGroup.group.groupId.in( +// JPAExpressions +// .select(group.groupId) +// .from(group) +// .where(group.user.userId.eq(rootUserId) +// .and(group.isDelete.eq(false))) +// )) +// )) +// .execute(); + } + + @Override + public Optional findUserByRootUserInGroup(Long rootUserId, Long subUserId) { + return Optional.ofNullable(queryFactory.select(user) + .from(user) + .join(userGroup) + .on(user.userId.eq(userGroup.user.userId)) + .join(group) + .on(userGroup.group.groupId.eq(group.groupId)) + .where(group.user.userId.eq(rootUserId).and(group.isDelete.isFalse()).and(user.userId.eq(subUserId)).and(user.isDelete.isFalse())) + .fetchOne()); + } + +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/UserService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/UserService.java index 948e5445..b09c0462 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/UserService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/UserService.java @@ -1,3 +1,174 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2875b5fd544945efcdd004575ef292d1cef8ad3945893fd8bd2b7f70300af9e0 -size 8691 +package com.dinnertime.peaktime.domain.user.service; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.domain.user.service.dto.request.*; +import com.dinnertime.peaktime.domain.user.service.dto.response.GetProfileResponse; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.AuthUtil; +import com.dinnertime.peaktime.global.util.RedisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserService { + + private final PasswordEncoder passwordEncoder; + private final RedisService redisService; + private final UserRepository userRepository; + + // 프로필 조회 + public GetProfileResponse getProfile(UserPrincipal userPrincipal) { + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + return GetProfileResponse.createGetProfileResponse(user.getUserLoginId(), user.getNickname(), user.getEmail()); + } + + // 닉네임 변경 + @Transactional + public void updateNickname(UpdateNicknameRequest updateNicknameRequest, UserPrincipal userPrincipal) { + // 1. 닉네임 형식 검사 + if(!AuthUtil.checkFormatValidationNickname(updateNicknameRequest.getNickname())) { + throw new CustomException(ErrorCode.INVALID_NICKNAME_FORMAT); + } + // 2. 유저 정보 불러오기 + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 3. 현재 유저의 닉네임과 비교하기 + if(updateNicknameRequest.getNickname().equals(user.getNickname())) { + throw new CustomException(ErrorCode.DUPLICATED_NICKNAME); + } + // 4. 유저 엔티티에 새로운 닉네임 집어넣기 + user.setNickname(updateNicknameRequest.getNickname()); + // 5. Save User + userRepository.save(user); + } + + // 회원탈퇴 + @Transactional + public void deleteUser(UserPrincipal userPrincipal) { + // 1. root 계정에게 종속된 child 계정 전부 탈퇴처리 + userRepository.updateIsDeleteByRootUserId(userPrincipal.getUserId()); + // 2. 이어서 root 계정 탈퇴처리 + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + user.deleteUser(); + userRepository.save(user); + // 3. root 계정과 이 root 계정에 종속된 child 계정의 User PK를 추출하여 Redis에 저장된 Refresh Token 삭제하기 (고도화) + } + + // 비밀번호 변경 + @Transactional + public void updatePassword(UpdatePasswordRequest updatePasswordRequest, UserPrincipal userPrincipal) { + // 1. 비밀번호 형식 검사 + if(!AuthUtil.checkFormatValidationPassword(updatePasswordRequest.getNewPassword())) { + throw new CustomException(ErrorCode.INVALID_PASSWORD_FORMAT); + } + // 2. 비밀번호 일치 검사 + if(!updatePasswordRequest.getNewPassword().equals(updatePasswordRequest.getConfirmNewPassword())) { + throw new CustomException(ErrorCode.NOT_EQUAL_PASSWORD); + } + // 3. 현재 유저 엔티티 불러오기 + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 4. 비밀번호 중복 검사 (현재 유저의 비밀번호와 비교하기) -> matches 메서드는 첫 번째 인자로 평문 비밀번호 필요 + if(passwordEncoder.matches(updatePasswordRequest.getNewPassword(), user.getPassword())) { + throw new CustomException(ErrorCode.DUPLICATED_PASSWORD); + } + // 5. 비밀번호 암호화 + String encodedPassword = passwordEncoder.encode(updatePasswordRequest.getNewPassword()); + // 6. 유저 엔티티에 새로운 비밀번호 집어넣기 + user.setPassword(encodedPassword); + // 7. Save User + userRepository.save(user); + } + + // 이메일 변경 + @Transactional + public void updateEmail(UpdateEmailRequest updateEmailRequest, UserPrincipal userPrincipal) { + // 1. 이메일 형식 검사 + if(!AuthUtil.checkFormatValidationEmail(updateEmailRequest.getEmail())) { + throw new CustomException(ErrorCode.INVALID_EMAIL_FORMAT); + } + // 2. 이메일 소문자로 변환 + String lowerEmail = AuthUtil.convertUpperToLower(updateEmailRequest.getEmail()); + // 3. 이메일 중복 검사 + if(this.checkDuplicateEmail(lowerEmail)) { + throw new CustomException(ErrorCode.DUPLICATED_EMAIL); + } + // 4. 현재 유저 엔티티 불러오기 + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 5. 현재 이메일 주소와 비교하기 -> 둘 다 소문자 변환 완료 + if(lowerEmail.equals(user.getEmail())) { + throw new CustomException(ErrorCode.SAME_EMAIL); + } + // 6. 이메일 인증여부 검사 + String redisEmailAuthentication = redisService.getEmailAuthentication(lowerEmail); + if(redisEmailAuthentication == null || !redisEmailAuthentication.equals("Authenticated")) { + throw new CustomException(ErrorCode.INVALID_EMAIL_AUTHENTICATION); + } + // 7. 유저 엔티티에 새로운 이메일 집어넣기 + user.setEmail(lowerEmail); + // 8. Save User + userRepository.save(user); + // 9. Redis에서 emailAuthentication prefix 데이터 삭제 + redisService.removeEmailAuthentication(lowerEmail); + } + + // 비밀번호 검증 + @Transactional(readOnly = true) + public void checkPassword(CheckPasswordRequest checkPasswordRequest, UserPrincipal userPrincipal) { + // 1. 유저 정보 불러오기 + User user = userRepository.findByUserId((userPrincipal.getUserId())) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 2. 비밀번호가 일치하는지 확인하기 + if(!passwordEncoder.matches(checkPasswordRequest.getPassword(), user.getPassword())) { + throw new CustomException(ErrorCode.INVALID_ROOT_PASSWORD); + } + } + + // 회원정보 관리 페이지 접근 권한 검사 + @Transactional(readOnly = true) + public void allowSettings(AllowSettingsRequest allowSettingsRequest, UserPrincipal userPrincipal) { + // 1. 유저 정보 불러오기 + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 2. Root 계정인지 Child 계정인지 판별하기 + if(!user.getIsRoot()) { + throw new CustomException(ErrorCode.SETTINGS_FOR_ROOT); + } + // 3. 클라이언트로부터 받은 비밀번호 형식 검사 + if(!AuthUtil.checkFormatValidationPassword(allowSettingsRequest.getPassword())) { + throw new CustomException(ErrorCode.INVALID_PASSWORD_FORMAT); + } + // 4. 비밀번호가 일치하는지 확인하기 + if(!passwordEncoder.matches(allowSettingsRequest.getPassword(), user.getPassword())) { + throw new CustomException(ErrorCode.INVALID_ROOT_PASSWORD); + } + } + + // 이메일 중복 검사 (이메일 주소로 검사. 이미 존재하면 true 반환) + private boolean checkDuplicateEmail(String lowerEmail) { + return userRepository.findByEmail(lowerEmail).isPresent(); + } + + @Transactional(readOnly = true) + public User getUser(Long userId) { + return userRepository.findByUserId(userId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/dto/response/GetProfileResponse.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/dto/response/GetProfileResponse.java index 453aeba7..cecc64d6 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/dto/response/GetProfileResponse.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/user/service/dto/response/GetProfileResponse.java @@ -1,3 +1,31 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3549e30b689f7fd3fc131354c092796707fa741bf5f328eaea24e598fa2226f4 -size 863 +package com.dinnertime.peaktime.domain.user.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GetProfileResponse { + + private String userLoginId; + private String nickname; + private String email; + + @Builder + private GetProfileResponse(String userLoginId, String nickname, String email) { + this.userLoginId = userLoginId; + this.nickname = nickname; + this.email = email; + } + + public static GetProfileResponse createGetProfileResponse(String userLoginId, String nickname, String email) { + return GetProfileResponse.builder() + .userLoginId(userLoginId) + .nickname(nickname) + .email(email) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/entity/UserGroup.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/entity/UserGroup.java index cb9de3f2..abbdf0c4 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/entity/UserGroup.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/entity/UserGroup.java @@ -1,3 +1,43 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ebbfecc3b85376619ef685c6c115f701d164a0f52f48b368cfc4fa57c90c1d41 -size 1120 +package com.dinnertime.peaktime.domain.usergroup.entity; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "users_groups") +@ToString +public class UserGroup { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="user_group_id") + private Long userGroupId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "child_user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_id", nullable = false) + private Group group; + + @Builder + private UserGroup(User user, Group group) { + this.user = user; + this.group = group; + } + + public static UserGroup createUserGroup(User user, Group group) { + return UserGroup.builder() + .user(user) + .group(group) + .build(); + } + + public void changeUserGroup(Group group) { + this.group = group; + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/repository/UserGroupRepository.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/repository/UserGroupRepository.java index a77998e3..465a7c33 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/repository/UserGroupRepository.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/domain/usergroup/repository/UserGroupRepository.java @@ -1,3 +1,22 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:840336df728dd662690258fd012a558e69528bc24318cd6a144629560d8f6386 -size 737 +package com.dinnertime.peaktime.domain.usergroup.repository; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserGroupRepository extends JpaRepository { + List findAllByGroup(Group group); + + // 그룹에 존재하는 유저 수 + Long countAllByGroup_groupId(Long groupId); + + // 자식 계정에 대한 user_group + Optional findByUser_UserId(Long userId); + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/controller/AuthController.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/controller/AuthController.java index 55ca4dd7..5852d2cb 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/controller/AuthController.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/controller/AuthController.java @@ -1,3 +1,234 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f232828de78cfffa014c1d2598801890a267cd3c636e023df945ca72f4e5587 -size 14908 +package com.dinnertime.peaktime.global.auth.controller; + +import com.dinnertime.peaktime.global.auth.service.dto.request.*; +import com.dinnertime.peaktime.global.auth.service.AuthService; +import com.dinnertime.peaktime.global.auth.service.dto.response.IsDuplicatedResponse; +import com.dinnertime.peaktime.global.auth.service.dto.response.LoginResponse; +import com.dinnertime.peaktime.global.auth.service.dto.response.ReissueResponse; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.util.CommonSwaggerResponse; +import com.dinnertime.peaktime.global.util.ResultDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + // 회원가입 + @Operation(summary = "회원가입", description = "회원가입하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원가입에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "403", description = "이메일이 인증되지 않았습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "이미 존재하는 아이디입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "일시적인 오류로 회원가입을 할 수 없습니다. 잠시 후 다시 이용해 주세요.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody @Valid SignupRequest signupRequest) { + authService.signup(signupRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "회원가입에 성공하였습니다.")); + } + + // 유저 로그인 아이디 중복 조회 + @Operation(summary = "유저 로그인 아이디 중복 조회", description = "유저 로그인 아이디 중복 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "유저 로그인 아이디 중복 조회 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = IsDuplicatedResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "유저 로그인 아이디 중복 조회 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/user-login-id") + public ResponseEntity isDuplicatedUserLoginId(@RequestParam(value = "userLoginId") String userLoginId) { + IsDuplicatedResponse response = authService.isDuplicatedUserLoginId(userLoginId); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "유저 로그인 아이디 중복 조회 요청에 성공하였습니다.", response)); + } + + // 이메일 중복 조회 + @Operation(summary = "이메일 중복 조회", description = "이메일 중복 조회하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "이메일 중복 조회 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = IsDuplicatedResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "이메일 중복 조회 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @GetMapping("/email") + public ResponseEntity isDuplicatedEmail(@RequestParam(value = "email") String email) { + IsDuplicatedResponse response = authService.isDuplicatedEmail(email); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "이메일 중복 조회 요청에 성공하였습니다.", response)); + } + + // 로그인 + @Operation(summary = "로그인", description = "로그인하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = LoginResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "등록되지 않은 아이디이거나 아이디 또는 비밀번호를 잘못 입력했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "일시적인 오류로 로그인을 할 수 없습니다. 잠시 후 다시 이용해 주세요.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/login") + public ResponseEntity login(@RequestBody @Valid LoginRequest loginRequest, HttpServletResponse httpServletResponse) { + LoginResponse response = authService.login(loginRequest, httpServletResponse); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "로그인에 성공하였습니다.", response)); + } + + // Reissue JWT + @Operation(summary = "JWT 재발급", description = "JWT 재발급하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "JWT가 재발급되었습니다.", + content = @Content(schema = @Schema(implementation = ReissueResponse.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "JWT 재발급 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/token/reissue") + public ResponseEntity reissue(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { + ReissueResponse response = authService.reissue(httpServletRequest, httpServletResponse); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "JWT가 재발급되었습니다.", response)); + } + + // 로그아웃 + @Operation(summary = "로그아웃", description = "로그아웃하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "비밀번호가 일치하지 않습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "로그아웃에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/logout") + public ResponseEntity logout(@RequestBody @Valid LogoutRequest logoutRequest, @AuthenticationPrincipal UserPrincipal userPrincipal, HttpServletResponse httpServletResponse) { + authService.logout(logoutRequest, userPrincipal, httpServletResponse); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "로그아웃에 성공하였습니다.")); + } + + // 인증 코드 전송 + @Operation(summary = "인증 코드 전송", description = "인증 코드 전송하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "인증 코드 전송에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "이메일을 전송하는데 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/code/send") + public ResponseEntity sendCode(@RequestBody @Valid SendCodeRequest sendCodeRequest) { + authService.sendCode(sendCodeRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "인증 코드 전송에 성공하였습니다.")); + } + + // 인증 코드 확인 + @Operation(summary = "인증 코드 확인", description = "인증 코드 확인하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "인증 코드 확인 요청에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "인증시간이 만료되었거나 인증코드를 발급받지 않으셨습니다. 다시 시도해주세요.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "인증 코드가 일치하지 않습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "인증 코드 확인 요청에 실패하였습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PostMapping("/code/check") + public ResponseEntity checkCode(@RequestBody @Valid CheckCodeRequest checkCodeRequest) { + authService.checkCode(checkCodeRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "인증 코드 확인 요청에 성공하였습니다.")); + } + + // 비밀번호 재발급 + @Operation(summary = "비밀번호 재발급", description = "비밀번호 재발급하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "비밀번호가 재발급되었습니다. 이메일을 확인해주세요.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 형식의 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "403", description = "서브 계정은 비밀번호 재발급 기능을 이용할 수 없습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 아이디입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "409", description = "아이디에 해당하는 이메일 주소가 아닙니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "500", description = "이메일을 전송하는데 실패했습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))) + }) + @CommonSwaggerResponse.CommonResponses + @PutMapping("/reset-password") + public ResponseEntity resetPassword(@RequestBody @Valid ResetPasswordRequest resetPasswordRequest) { + authService.resetPassword(resetPasswordRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(ResultDto.res(HttpStatus.OK.value(), + "비밀번호가 재발급되었습니다. 이메일을 확인해주세요.")); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/ExceptionHandlerFilter.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/ExceptionHandlerFilter.java index 39331dd3..edb40022 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/ExceptionHandlerFilter.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/ExceptionHandlerFilter.java @@ -1,3 +1,41 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9a1ba9785cfdea9b21934b3299f28a331e5089897b5c039abb6e425a90a87394 -size 1706 +package com.dinnertime.peaktime.global.auth.filter; + +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.util.ResultDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +public class ExceptionHandlerFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(httpServletRequest, httpServletResponse); + } catch (CustomException e) { + setErrorResponse(e.getErrorCode().getHttpStatus(), httpServletResponse, e); + } catch (Exception e) { + setErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, httpServletResponse, e); + } + } + + private void setErrorResponse(HttpStatus status, HttpServletResponse httpServletResponse, Throwable ex) throws IOException { + + // response error 헤더 통일 시켜주기 + httpServletResponse.setStatus(status.value()); + httpServletResponse.setContentType("application/json; charset=utf-8"); + + ResultDto errorResponse = ResultDto.res(status.value(), ex.getMessage()); + + httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/JwtFilter.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/JwtFilter.java index 05003c66..fdaaefbb 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/JwtFilter.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/filter/JwtFilter.java @@ -1,3 +1,75 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3168c250243f2aaa015d4b242c17420abca53ca63b87cd0d12abf365e005181e -size 3472 +package com.dinnertime.peaktime.global.auth.filter; + +import com.dinnertime.peaktime.domain.user.service.UserService; +import com.dinnertime.peaktime.global.auth.service.JwtService; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +public class JwtFilter extends GenericFilterBean { + + private final JwtService jwtService; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + log.info("JwtFilter START"); + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String jwt = null; + + // 1. Request Header에서 Access Token 추출 + String bearerToken = httpServletRequest.getHeader("Authorization"); + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + jwt = bearerToken.substring(7); + } + + // 2. Access Token 유효성 검증 (위변조, 만료 등) + if((!StringUtils.hasText(jwt)) || (!jwtService.validateToken(jwt))) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + // 3. Access Token에서 User PK와 Authority 정보 추출 + long userId = jwtService.getUserId(jwt); + String authority = jwtService.getAuthority(jwt); + + log.info(authority); + + // 4. SecurityContextHolder의 Authentication의 Principal에 저장할 객체 생성 (개발자용) + UserPrincipal userPrincipal = UserPrincipal.createUserPrincipal(userId, null, authority); + + // 5. SecurityContextHolder의 Authentication의 Authorities에 저장할 객체 생성 (필터용) + List authorities = List.of(new SimpleGrantedAuthority(authority)); + + // 6. 실제 등록할 수 있는 Authentication 타입의 객체 생성 (UsernamePasswordAuthenticationToken은 Principal, Credentials, Authorities 필드 존재) + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities); + + // 7. 이후의 인증 절차에서 세션 관련 정보나 IP 정보를 사용 가능 + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); + + // 8. 등록하기 + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 9. 다음 필터로 요청 + filterChain.doFilter(servletRequest, servletResponse); + + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/AuthService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/AuthService.java index 1c32db98..4e773833 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/AuthService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/AuthService.java @@ -1,3 +1,388 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac8228b4e186dee179ba5dc3fd93f63868d8e1626c06d2ba1fc2ff63940fc6c5 -size 20018 +package com.dinnertime.peaktime.global.auth.service; + +import com.dinnertime.peaktime.domain.group.entity.Group; +import com.dinnertime.peaktime.domain.group.repository.GroupRepository; +import com.dinnertime.peaktime.domain.preset.entity.Preset; +import com.dinnertime.peaktime.domain.preset.repository.PresetRepository; +import com.dinnertime.peaktime.domain.statistic.entity.Statistic; +import com.dinnertime.peaktime.domain.statistic.repository.StatisticRepository; +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.global.auth.service.dto.request.*; +import com.dinnertime.peaktime.domain.usergroup.entity.UserGroup; +import com.dinnertime.peaktime.domain.usergroup.repository.UserGroupRepository; +import com.dinnertime.peaktime.global.auth.service.dto.response.IsDuplicatedResponse; +import com.dinnertime.peaktime.global.auth.service.dto.response.LoginResponse; +import com.dinnertime.peaktime.global.auth.service.dto.response.ReissueResponse; +import com.dinnertime.peaktime.global.auth.service.dto.security.UserPrincipal; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.EmailServerException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.AuthUtil; +import com.dinnertime.peaktime.global.util.EmailService; +import com.dinnertime.peaktime.global.util.RedisService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.io.*; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final RedisService redisService; + private final EmailService emailService; + private final UserRepository userRepository; + private final PresetRepository presetRepository; + private final GroupRepository groupRepository; + private final UserGroupRepository userGroupRepository; + private final StatisticRepository statisticRepository; + + // 회원가입 + @Transactional + public void signup(SignupRequest signupRequest) { + // 1-1. 아이디 형식 검사 + if(!AuthUtil.checkFormatValidationUserLoginId(signupRequest.getUserLoginId())) { + throw new CustomException(ErrorCode.INVALID_USER_LOGIN_ID_FORMAT); + } + // 1-2. 아이디 중복 검사 + if(this.checkDuplicateUserLoginId(signupRequest.getUserLoginId())) { + throw new CustomException(ErrorCode.DUPLICATED_USER_LOGIN_ID); + } + // 2-1. 비밀번호 형식 검사 + if(!AuthUtil.checkFormatValidationPassword(signupRequest.getPassword())) { + throw new CustomException(ErrorCode.INVALID_PASSWORD_FORMAT); + } + // 2-2. 비밀번호 일치 검사 + if(!signupRequest.getPassword().equals(signupRequest.getConfirmPassword())) { + throw new CustomException(ErrorCode.NOT_EQUAL_PASSWORD); + } + // 2-3. 비밀번호 암호화 + String encodedPassword = passwordEncoder.encode(signupRequest.getPassword()); + // 3. 닉네임 형식 검사 + if(!AuthUtil.checkFormatValidationNickname(signupRequest.getNickname())) { + throw new CustomException(ErrorCode.INVALID_NICKNAME_FORMAT); + } + // 4-1. 이메일 형식 검사 + if(!AuthUtil.checkFormatValidationEmail(signupRequest.getEmail())) { + throw new CustomException(ErrorCode.INVALID_EMAIL_FORMAT); + } + // 4-2. 이메일 소문자로 변환 + String lowerEmail = AuthUtil.convertUpperToLower(signupRequest.getEmail()); + // 4-3. 이메일 중복 검사 + if(this.checkDuplicateEmail(lowerEmail)) { + throw new CustomException(ErrorCode.DUPLICATED_EMAIL); + } + // 4-4. 이메일 인증여부 검사 + String redisEmailAuthentication = redisService.getEmailAuthentication(lowerEmail); + if(redisEmailAuthentication == null || !redisEmailAuthentication.equals("Authenticated")) { + throw new CustomException(ErrorCode.INVALID_EMAIL_AUTHENTICATION); + } + // 5. Create User Entity + User user = User.createRootUser( + signupRequest.getUserLoginId(), + encodedPassword, + signupRequest.getNickname(), + lowerEmail + ); + // 6. Save User + userRepository.save(user); + //6-1. Save statistic + Statistic statistic = Statistic.createFirstStatistic(user); + statisticRepository.save(statistic); + // 7. Redis에서 emailAuthenticaion prefix 데이터 삭제 + redisService.removeEmailAuthentication(lowerEmail); + // 8. Create Block Website Array For Default Preset + List blockWebsiteList; + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("DistractionsWebsites.txt")) { + if (inputStream == null) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + blockWebsiteList = new BufferedReader(new InputStreamReader(inputStream)) + .lines() + .collect(Collectors.toList()); + } catch (IOException e) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + // 9. Create Block Program Array For Default Preset + List blockProgramList; + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("DistractionsPrograms.txt")) { + if (inputStream == null) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + blockProgramList = new BufferedReader(new InputStreamReader(inputStream)) + .lines() + .collect(Collectors.toList()); + } catch (IOException e) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + + // 10. Create Default Preset + Preset preset = Preset.createDefaultPreset(blockWebsiteList, blockProgramList, user); + // 11. Save Preset + presetRepository.save(preset); + } + + // 유저 로그인 아이디 중복 조회 + public IsDuplicatedResponse isDuplicatedUserLoginId(String userLoginId) { + // 1. 아이디 형식 검사 + if(!AuthUtil.checkFormatValidationUserLoginId(userLoginId)) { + throw new CustomException(ErrorCode.INVALID_USER_LOGIN_ID_FORMAT); + } + // 2. 아이디 중복 검사 + boolean isDuplicated = this.checkDuplicateUserLoginId(userLoginId); + return IsDuplicatedResponse.createIsDuplicatedResponse(isDuplicated); + } + + // 이메일 중복 조회 + public IsDuplicatedResponse isDuplicatedEmail(String email) { + // 1. 이메일 형식 검사 + if(!AuthUtil.checkFormatValidationEmail(email)) { + throw new CustomException(ErrorCode.INVALID_EMAIL_FORMAT); + } + // 2. 이메일 소문자로 변환 + String lowerEmail = AuthUtil.convertUpperToLower(email); + // 3. 이메일 중복 검사 + boolean isDuplicated = this.checkDuplicateEmail(lowerEmail); + return IsDuplicatedResponse.createIsDuplicatedResponse(isDuplicated); + } + + // 로그인 + @Transactional(readOnly = true) + public LoginResponse login(LoginRequest loginRequest, HttpServletResponse httpServletResponse) { + // 1. loadUserByUsername 메서드 호출 + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + loginRequest.getUserLoginId(), + loginRequest.getPassword() + ) + ); + // 2. 현재 SecurityContextHolder의 Authentication의 Principal 불러오기 + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + // 3. PK와 Authority 정보를 추출하여 JWT 생성 + String accessToken = jwtService.createAccessToken(userPrincipal.getUserId(), userPrincipal.getAuthority()); + String refreshToken = jwtService.createRefreshToken(userPrincipal.getUserId(), userPrincipal.getAuthority()); + // 4. Refresh Token을 Redis에 저장 + redisService.saveRefreshToken(userPrincipal.getUserId(), refreshToken); + // 5. Refresh Token을 Cookie에 담아서 클라이언트에게 전송 + jwtService.addRefreshTokenToCookie(httpServletResponse, refreshToken); + // 6. LoginResponse 객체 생성하여 반환 + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.INVALID_LOGIN_PROCESS)); + if(user.getIsRoot()) { + return LoginResponse.createLoginResponse(accessToken, true, null, user.getNickname()); + } + UserGroup userGroup = userGroupRepository.findByUser_UserId(user.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.INVALID_LOGIN_PROCESS)); + return LoginResponse.createLoginResponse(accessToken, false, userGroup.getGroup().getGroupId(), user.getNickname()); + } + + // Reissue JWT + @Transactional + public ReissueResponse reissue(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { + // 1. 클라이언트의 요청에서 Refresh Token 추출하기 + String refreshToken = jwtService.extractRefreshToken(httpServletRequest); + // 2. Refresh Token 유효성 검증 (위변조, 만료 등) -> 유효하지 않으면 401 예외 던지기 + if((!StringUtils.hasText(refreshToken)) || (!jwtService.validateToken(refreshToken))) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + // 3. Redis에 존재하는 Refresh Token과 일치하는지 확인하기 -> 일치하지 않으면 401 예외 던지기 + long userId = jwtService.getUserId(refreshToken); + String redisRefreshToken = redisService.getRefreshToken(userId); + if(!refreshToken.equals(redisRefreshToken)) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + // 4. 새 Access Token과 새 Refresh Token을 생성하기 위한 정보를 기존 Refresh Token에서 추출하기 (userId, Authority, 만료시간) + String authority = jwtService.getAuthority(refreshToken); + Date expirationTime = jwtService.getExpirationTime(refreshToken); + // 5. 새 Access Token과 새 Refresh Token 생성 + String newAccessToken = jwtService.createAccessToken(userId, authority); + String newRefreshToken = jwtService.createRefreshTokenWithExp(userId, authority, expirationTime); + // 6. 새 Refresh Token을 Redis에 저장 (기존 Refresh Token 반드시 덮어쓰기) + redisService.saveRefreshToken(userId, newRefreshToken); + // 7. 새 Refresh Token을 Cookie에 담아서 클라이언트에게 전송 + jwtService.addRefreshTokenToCookie(httpServletResponse, newRefreshToken); + // 8. ReissueResponse 객체 생성하여 반환 (새 Access Token을 Response Body에 담아서 클라이언트에게 전송) + return ReissueResponse.createReissueResponse(newAccessToken); + } + + // 로그아웃 + @Transactional + public void logout(LogoutRequest logoutRequest, UserPrincipal userPrincipal, HttpServletResponse httpServletResponse) { + // 1. 클라이언트의 요청에서 rootUserPassword 추출하기 + String rootUserPassword = logoutRequest.getRootUserPassword(); + // 2. DB에서 비밀번호 가져오기 + String rootUserPasswordOnDatabase = this.getRootUserPasswordOnDatabase(userPrincipal); + // 3. 비밀번호 비교하기 -> matches 메서드는 첫 번째 인자로 평문 비밀번호 필요 + if(!passwordEncoder.matches(rootUserPassword, rootUserPasswordOnDatabase)) { + throw new CustomException(ErrorCode.INVALID_ROOT_PASSWORD); + } + // 4. Redis에서 해당 유저의 Refresh Token 삭제 + redisService.removeRefreshToken(userPrincipal.getUserId()); + // 5. 클라이언트의 Refresh Token 삭제 + jwtService.letRefreshTokenRemoved(httpServletResponse); + } + + // 인증 코드 전송 + @Transactional + public void sendCode(SendCodeRequest sendCodeRequest) { + // 1. 인증 코드 생성 + String code = this.generateCode(); + // 2. 클라이언트에게 받은 이메일 주소로 인증 코드 보내기 + String lowerEmail = AuthUtil.convertUpperToLower(sendCodeRequest.getEmail()); + emailService.sendCode(lowerEmail, code); + // 3. Redis에 Key가 emailCode라는 prefix와 이메일 주소(소문자)로 이루어져 있고, Value가 랜덤 인증 코드인 정보를 저장하기 + redisService.saveEmailCode(lowerEmail, code); + } + + // 인증 코드 확인 + @Transactional + public void checkCode(CheckCodeRequest checkCodeRequest) { + // 1. 클라이언트의 요청에서 email과 code를 추출하기 + String lowerEmail = AuthUtil.convertUpperToLower(checkCodeRequest.getEmail()); + String code = checkCodeRequest.getCode(); + // 2. 클라이언트에게 받은 email에 대응하는 Redis 데이터를 조회하고 비교하기 + String redisEmailCode = redisService.getEmailCode(lowerEmail); + if(redisEmailCode == null) { + throw new CustomException(ErrorCode.EMAIL_CODE_NOT_FOUND); + } + if(!code.equals(redisEmailCode)) { + throw new CustomException(ErrorCode.NOT_EQUAL_EMAIL_CODE); + } + // 3. 인증 코드가 일치하면, 우선 Redis에서 필요없는 데이터 삭제하기 + redisService.removeEmailCode(lowerEmail); + // 4. Redis에 Key가 emailAuthentication이라는 prefix와 이메일 주소(소문자)로 이루어져 있고, Value가 "Authenticated"인 정보를 저장하기 (만료시간 X) + redisService.saveEmailAuthentication(lowerEmail); + } + + // 비밀번호 재발급 + @Transactional + public void resetPassword(ResetPasswordRequest resetPasswordRequest) { + // 1. 클라이언트의 요청에서 userLoginId과 email 추출하기 + String userLoginId = resetPasswordRequest.getUserLoginId(); + String lowerEmail = AuthUtil.convertUpperToLower(resetPasswordRequest.getEmail()); + // 2. 아이디 형식 검사 + if(!AuthUtil.checkFormatValidationUserLoginId(userLoginId)) { + throw new CustomException(ErrorCode.INVALID_USER_LOGIN_ID_FORMAT); + } + // 3. 아이디 존재 검사 + User user = userRepository.findByUserLoginId(userLoginId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_LOGIN_ID_NOT_FOUND)); + // 4. 루트 계정만 이용 가능 + if(!user.getIsRoot()) { + throw new CustomException(ErrorCode.NOT_ROOT); + } + // 5. 이메일 비교하기 + if(!lowerEmail.equals(user.getEmail())) { + throw new CustomException(ErrorCode.NOT_EQUAL_EMAIL); + } + // 6. 랜덤 비밀번호 생성 + String password = this.generatePassword(); + // 7. 클라이언트에게 받은 이메일 주소로 랜덤 비밀번호 발송 + emailService.sendPassword(lowerEmail, password); + // 8. 암호화한 랜덤 비밀번호를 유저 엔티티에 집어넣기 + String encodedPassword = passwordEncoder.encode(password); + user.setPassword(encodedPassword); + // 9. Save User + userRepository.save(user); + } + + // 아이디 중복 검사 (유저 로그인 아이디로 검사. 이미 존재하면 true 반환) + private boolean checkDuplicateUserLoginId(String userLoginId) { + return userRepository.findByUserLoginId(userLoginId).isPresent(); + } + + // 이메일 중복 검사 (이메일 주소로 검사. 이미 존재하면 true 반환) + private boolean checkDuplicateEmail(String lowerEmail) { + return userRepository.findByEmail(lowerEmail).isPresent(); + } + + // 데이터베이스에 존재하는 루트 계정 비밀번호 가져오기 + private String getRootUserPasswordOnDatabase(UserPrincipal userPrincipal) { + if(userPrincipal.getAuthority().equals("child")) { + UserGroup userGroup = userGroupRepository.findByUser_UserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.DO_NOT_HAVE_USERGROUP)); + Group group = groupRepository.findByGroupId(userGroup.getGroup().getGroupId()) + .orElseThrow(() -> new CustomException(ErrorCode.DO_NOT_HAVE_GROUP)); + return group.getUser().getPassword(); + } + User user = userRepository.findByUserId(userPrincipal.getUserId()) + .orElseThrow(() -> new CustomException(ErrorCode.DO_NOT_HAVE_USER)); + return user.getPassword(); + } + + // 인증 코드 생성 + private String generateCode() { + SecureRandom secureRandom = new SecureRandom(); + StringBuilder sb = new StringBuilder(6); + for(int i = 0; i < 6; i++) { + sb.append(secureRandom.nextInt(10)); + } + return sb.toString(); + } + + // 비밀번호 생성 + private String generatePassword() { + String uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + String lowercase = "abcdefghijklmnopqrstuvwxyz"; + String digits = "0123456789"; + String specialCharacters = "!@#$%^&*"; + List passwordCharacters = new ArrayList<>(); + SecureRandom secureRandom = new SecureRandom(); + + // 각 문자 집합에서 최소 하나씩 추가 + passwordCharacters.add(this.getRandomCharacter(uppercase)); + passwordCharacters.add(this.getRandomCharacter(lowercase)); + passwordCharacters.add(this.getRandomCharacter(digits)); + passwordCharacters.add(this.getRandomCharacter(specialCharacters)); + + // 나머지 자리는 네 그룹을 합친 문자열에서 랜덤하게 선택하여 4자리 추가 + String allCharacters = uppercase + lowercase + digits + specialCharacters; + for(int i = 0; i < 4; i++) { + passwordCharacters.add(this.getRandomCharacter(allCharacters)); + } + + // 셔플하여 순서 섞기 + Collections.shuffle(passwordCharacters, secureRandom); + + // 리스트를 문자열로 변환하여 반환 + StringBuilder password = new StringBuilder(); + for(Character ch : passwordCharacters) { + password.append(ch); + } + return password.toString(); + } + + // 비밀번호 생성 시 랜덤 문자 선택 메서드 + private char getRandomCharacter(String characters) { + SecureRandom secureRandom = new SecureRandom(); + int index = secureRandom.nextInt(characters.length()); + return characters.charAt(index); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/JwtService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/JwtService.java index e041ae64..39a405ab 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/JwtService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/JwtService.java @@ -1,3 +1,169 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57e770958786dd7109b85884b75ef4824410b8f5a8e9b4fccf3e888188904ffc -size 5864 +package com.dinnertime.peaktime.global.auth.service; + +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.time.Instant; +import java.util.Base64; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtService implements InitializingBean { + + // JWT 만료 시간 + @Value("${jwt.token.access-expire-time}") + private int ACCESSTOKEN_EXPIRE_TIME; + @Value("${jwt.token.refresh-expire-time}") + private int REFRESHTOKEN_EXPIRE_TIME; + + // JWT Signature 생성에 사용되는 문자열(서버만 알고 있는 비밀번호) -> a + @Value("${jwt.token.secret-key}") + private String SECRET_KEY; + + // JWT 서명에 사용되는 SecretKey 객체 -> b + private SecretKey secretKey; + + @Override + public void afterPropertiesSet() { + this.secretKey = buildKey(); + } + + // a를 b로 변환하는 메서드 + private SecretKey buildKey() { + byte[] decodedKeyValue = Base64.getDecoder().decode(SECRET_KEY); // Decoder 사용 + return Keys.hmacShaKeyFor(decodedKeyValue); + } + + // Access Token 생성 + public String createAccessToken(long userId, String authority) { + Instant now = Instant.now(); + return Jwts.builder() + .claim("userId", userId) + .claim("authority", authority) + .signWith(secretKey) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(now.plusSeconds(ACCESSTOKEN_EXPIRE_TIME))) + .compact(); + } + + // Refresh Token 생성 + public String createRefreshToken(long userId, String authority) { + Instant now = Instant.now(); + return Jwts.builder() + .claim("userId", userId) + .claim("authority", authority) + .signWith(secretKey) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(now.plusSeconds(REFRESHTOKEN_EXPIRE_TIME))) + .compact(); + } + + // 만료시점을 지정할 수 있는 Refresh Token 생성 + public String createRefreshTokenWithExp(long userId, String authority, Date expirationTime) { + Instant now = Instant.now(); + return Jwts.builder() + .claim("userId", userId) + .claim("authority", authority) + .signWith(secretKey) + .setIssuedAt(Date.from(now)) + .setExpiration(expirationTime) + .compact(); + } + + // Cookie에 Refresh Token 담기 + public void addRefreshTokenToCookie(HttpServletResponse httpServletResponse, String refreshToken) { + Cookie cookie = new Cookie("refresh_token", refreshToken); + // 어느 페이지에서도 유효 + cookie.setPath("/"); + cookie.setMaxAge(REFRESHTOKEN_EXPIRE_TIME); + cookie.setHttpOnly(true); + cookie.setSecure(true); + httpServletResponse.addCookie(cookie); + } + + // 클라이언트의 Refresh Token 삭제 + public void letRefreshTokenRemoved(HttpServletResponse httpServletResponse) { + Cookie cookie = new Cookie("refresh_token", "Arbitrary"); + cookie.setPath("/"); + cookie.setMaxAge(0); + cookie.setHttpOnly(true); + cookie.setSecure(true); + httpServletResponse.addCookie(cookie); + } + + // JWT 유효성 검사 + public boolean validateToken(String token) { + try { + // 유효한 토큰이라면 Claims 객체를 추출 가능 + Claims claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + return true; + } catch (Exception e) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + } + + // JWT에서 User PK 추출 + public long getUserId(String token) { + Claims claims = this.getClaims(token); + Number userId = (Number) claims.get("userId"); + return userId.longValue(); + } + + // JWT에서 Authority 추출 + public String getAuthority(String token) { + Claims claims = this.getClaims(token); + return (String) claims.get("authority"); + } + + // JWT에서 만료시간 추출 + public Date getExpirationTime(String token) { + Claims claims = this.getClaims(token); + return claims.getExpiration(); + } + + // JWT에서 Claims 추출 + private Claims getClaims(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + } + + // 클라이언트의 요청에서 Refresh Token 추출하기 + public String extractRefreshToken(HttpServletRequest httpServletRequest) { + Cookie[] cookies = httpServletRequest.getCookies(); + if(cookies == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + for(Cookie cookie : cookies) { + if(cookie.getName().equals("refresh_token")) { + return cookie.getValue(); + } + } + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/LoginRequest.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/LoginRequest.java index 7a175cb5..bd4e5f6a 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/LoginRequest.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/LoginRequest.java @@ -1,3 +1,20 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a1ac04de3753a5b07b4724290d57b449f13b26ce03c74b9d5d89a6e02bda1232 -size 600 +package com.dinnertime.peaktime.global.auth.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginRequest { + + // `/auth/login` API를 호출할 때, Response Body에 빈 값이 오면 MethodArgumentNotValidException가 발생. + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String userLoginId; + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String password; + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/SignupRequest.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/SignupRequest.java index 605af411..71dd4452 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/SignupRequest.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/request/SignupRequest.java @@ -1,3 +1,30 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:454dd626b11e99c42bbbd0f3bff4690f344c4036b39d3bd53985be5d39e30fb2 -size 922 +package com.dinnertime.peaktime.global.auth.service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SignupRequest { + + // `/auth/signup` API를 호출할 때, Response Body에 빈 값이 오면 MethodArgumentNotValidException가 발생. + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String userLoginId; + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String password; + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String confirmPassword; + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String nickname; + + @NotBlank(message = "잘못된 형식의 요청입니다.") + private String email; + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/IsDuplicatedResponse.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/IsDuplicatedResponse.java index fc99b4a6..9c65737d 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/IsDuplicatedResponse.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/IsDuplicatedResponse.java @@ -1,3 +1,25 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ac8038c07622b05067cac66f09261eac426cff6291cd11f07b31a840812c8e8 -size 638 +package com.dinnertime.peaktime.global.auth.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IsDuplicatedResponse { + + private Boolean isDuplicated; + + @Builder + private IsDuplicatedResponse(Boolean isDuplicated) { + this.isDuplicated = isDuplicated; + } + + public static IsDuplicatedResponse createIsDuplicatedResponse(Boolean isDuplicated) { + return IsDuplicatedResponse.builder() + .isDuplicated(isDuplicated) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/LoginResponse.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/LoginResponse.java index 33805273..127a749f 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/LoginResponse.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/LoginResponse.java @@ -1,3 +1,34 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9087499d132fb6b232e3b55a2ef09628f0b466bbc4f60b6eab4fc1c2f5cdabbb -size 968 +package com.dinnertime.peaktime.global.auth.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginResponse { + + private String accessToken; + private Boolean isRoot; + private Long groupId; + private String nickname; + + @Builder + private LoginResponse(String accessToken, Boolean isRoot, Long groupId, String nickname) { + this.accessToken = accessToken; + this.isRoot = isRoot; + this.groupId = groupId; + this.nickname = nickname; + } + + public static LoginResponse createLoginResponse(String accessToken, Boolean isRoot, Long groupId, String nickname) { + return LoginResponse.builder() + .accessToken(accessToken) + .isRoot(isRoot) + .groupId(groupId) + .nickname(nickname) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/ReissueResponse.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/ReissueResponse.java index fa02b933..caadbe22 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/ReissueResponse.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/response/ReissueResponse.java @@ -1,3 +1,25 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d548f4b0c880faa2cf03ae802e88daf53652b2bf4485515b31798cbaf549774d -size 603 +package com.dinnertime.peaktime.global.auth.service.dto.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ReissueResponse { + + private String accessToken; + + @Builder + private ReissueResponse(String accessToken) { + this.accessToken = accessToken; + } + + public static ReissueResponse createReissueResponse(String accessToken) { + return ReissueResponse.builder() + .accessToken(accessToken) + .build(); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserDetailsServiceImpl.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserDetailsServiceImpl.java index 2eb2f709..9859e9d0 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserDetailsServiceImpl.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserDetailsServiceImpl.java @@ -1,3 +1,32 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b233ed7dd4cf7df1c1f5163e69a423d3651f65c27ac85b409429a7f60b5e7ec4 -size 1470 +package com.dinnertime.peaktime.global.auth.service.dto.security; + +import com.dinnertime.peaktime.domain.user.entity.User; +import com.dinnertime.peaktime.domain.user.repository.UserRepository; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 1. 클라이언트에게 전달받은 아이디가 존재하는지 확인 + User user = userRepository.findByUserLoginIdAndIsDeleteFalse(username) + .orElseThrow(() -> new CustomException(ErrorCode.INVALID_LOGIN_PROCESS)); + // 2. 해당 유저의 권한 파악 + if(user.getIsRoot()) { + return UserPrincipal.createUserPrincipal(user.getUserId(), user.getPassword(), "root"); + } + return UserPrincipal.createUserPrincipal(user.getUserId(), user.getPassword(), "child"); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserPrincipal.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserPrincipal.java index d1429b8c..5e9d075d 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserPrincipal.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/auth/service/dto/security/UserPrincipal.java @@ -1,3 +1,45 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2a184d1fce833ee110732d9484a7a0a153a1de2c34b245209601a984ef43d656 -size 1236 +package com.dinnertime.peaktime.global.auth.service.dto.security; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserPrincipal implements UserDetails { + + private long userId; + private String password; + private String authority; // "root" or "child" + + @Builder + private UserPrincipal(long userId, String password, String authority) { + this.userId = userId; + this.password = password; + this.authority = authority; + } + + public static UserPrincipal createUserPrincipal(long userId, String password, String authority) { + return UserPrincipal.builder() + .userId(userId) + .password(password) + .authority(authority) + .build(); + } + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getUsername() { + return ""; + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/EmailConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/EmailConfig.java index 8c8708ac..0bd272ba 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/EmailConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/EmailConfig.java @@ -1,3 +1,42 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e1485c743e5cd2bdcede427a18a5938aa0ab182d650c1d8a9478a1128d35f47 -size 1626 +package com.dinnertime.peaktime.global.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Slf4j +@Configuration +public class EmailConfig { + + @Value("${spring.mail.tx}") + private String tx; + @Value("${spring.mail.password}") + private String password; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl(); + javaMailSenderImpl.setHost("smtp.gmail.com"); + javaMailSenderImpl.setPort(587); // TLS를 사용하는 SMTP 서버의 기본포트는 587 + javaMailSenderImpl.setUsername(this.tx); + javaMailSenderImpl.setPassword(this.password); + + Properties javaMailProperties = new Properties(); + javaMailProperties.put("mail.transport.protocol", "smtp"); + javaMailProperties.put("mail.smtp.auth", "true"); + javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + javaMailProperties.put("mail.smtp.starttls.enable", "true"); + javaMailProperties.put("mail.debug", "true"); + javaMailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com"); + javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.3"); + + javaMailSenderImpl.setJavaMailProperties(javaMailProperties); + return javaMailSenderImpl; + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/OpenAiConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/OpenAiConfig.java index e9934269..8f0c98ce 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/OpenAiConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/OpenAiConfig.java @@ -1,3 +1,21 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65dede1306654ff66dbe650475a571388b2f3a1c387b8cffeefdc8868c4a723f -size 767 +package com.dinnertime.peaktime.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OpenAiConfig { + @Value("${openai.api.key}") + private String openAiKey; + @Bean(name = "openAiRestTemplate") + public RestTemplate restTemplate(){ + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> { + request.getHeaders().add("Authorization", "Bearer " + openAiKey); + return execution.execute(request, body); + }); + return restTemplate; + } +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/RedisConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/RedisConfig.java index b5f26542..64353cd1 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/RedisConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/RedisConfig.java @@ -1,3 +1,39 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:319d35b26b1baaaf15d627863c88671b577ddbdf6e3b4a7d79ee961c8d776409 -size 1644 +package com.dinnertime.peaktime.global.config; + +import com.dinnertime.peaktime.domain.schedule.service.dto.RedisSchedule; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.util.List; + +@Configuration +public class RedisConfig { + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + redisTemplate.setDefaultSerializer(serializer); + + // Custom ObjectMapper 설정 + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + // Key Serializer + redisTemplate.setKeySerializer(new StringRedisSerializer()); + + // Value Serializer + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SchedulingConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SchedulingConfig.java index 0c9dc257..be5a3cfc 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SchedulingConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SchedulingConfig.java @@ -1,3 +1,21 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92d2a0b233c95c256307e18b687debc28e28e547fb9a884f3ac95831e839d885 -size 826 +package com.dinnertime.peaktime.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +//멀티스레드를 위한 설정 +@EnableScheduling +@Configuration +public class SchedulingConfig implements SchedulingConfigurer { + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setPoolSize(10); + taskScheduler.initialize(); + taskRegistrar.setTaskScheduler(taskScheduler); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SecurityConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SecurityConfig.java index 48f75c60..12369f1f 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SecurityConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SecurityConfig.java @@ -1,3 +1,76 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e2f6e5128b42422dab0c7394c612c09336a6bc70f3a2b9288b4f41da397dbf2 -size 3977 +package com.dinnertime.peaktime.global.config; + +import com.dinnertime.peaktime.domain.user.service.UserService; +import com.dinnertime.peaktime.global.auth.filter.ExceptionHandlerFilter; +import com.dinnertime.peaktime.global.auth.filter.JwtFilter; +import com.dinnertime.peaktime.global.auth.service.JwtService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.ExceptionTranslationFilter; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Slf4j +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity +public class SecurityConfig { + + private final JwtService jwtService; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager( + final AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http.cors(withDefaults()) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers(HttpMethod.GET, "/presets").hasAnyAuthority("root","child") + .requestMatchers(HttpMethod.GET, "/schedules").hasAuthority("child") + .requestMatchers("/auth/logout","/hikings/**","/summaries/**", "/memos/**", "/users/password").hasAnyAuthority("root", "child") + .requestMatchers("/users/**", "/children/**", "/groups/**", "/presets/**", "/timers/**").hasAuthority("root") + .anyRequest().denyAll()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .addFilterBefore(new JwtFilter(jwtService), ExceptionTranslationFilter.class) + .addFilterBefore(new ExceptionHandlerFilter(), JwtFilter.class) + .build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + // 아래 url은 filter 에서 제외 + return web -> + web.ignoring() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**") + .requestMatchers("/auth/login", "/auth/signup", "/auth/user-login-id", "/auth/email", "/auth/code/**", "/auth/reset-password", "/auth/token/reissue"); + // 테스트가 끝나면 "/auth/login"을 추가해주세요. + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SwaggerConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SwaggerConfig.java index 754b063d..944ca5a5 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SwaggerConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/SwaggerConfig.java @@ -1,3 +1,39 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72d08d3bd32844a69aab0e28c596209bfdeb813519fd13875b3ae4c4f899069b -size 1751 +package com.dinnertime.peaktime.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + // bearer token 설정하기 + .components(new Components().addSecuritySchemes("bearer-key", + new SecurityScheme().type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))) + .addSecurityItem(new SecurityRequirement().addList("bearer-key")) + // 문서 설명 + .info(new Info().title("PeakTime API") + .description("PeakTime API 문서") + .version("1.0")) + // 첫 번째 url -> http 서버 연결하는 방식 + // 두 번째 url -> https 서버 연결하는 방식 + // 세 번째 url -> local 서버 연결하는 방식 + + // 서버 URL을 HTTPS로 설정 및 기본 경로 추가 + .servers(Arrays.asList( + new Server().url("https://peaktime.me/api/v1"), + new Server().url("http://localhost:8080/api/v1"))); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/WebMvcConfig.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/WebMvcConfig.java index 32258cff..3c29adf1 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/WebMvcConfig.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/config/WebMvcConfig.java @@ -1,3 +1,19 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3109b38e6837343a47403d41ee17c7bdacac5d1e14a319d23d37a03749234034 -size 677 +package com.dinnertime.peaktime.global.config; + + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "PATCH","DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3000); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorCode.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorCode.java index f6f9c3ee..819cf7ea 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorCode.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorCode.java @@ -1,3 +1,71 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:058a7f13c6462c2e19e8227d4d0530fd3b0aaa286be21fdeaaaac3567f69d67f -size 5340 +package com.dinnertime.peaktime.global.exception; + +import org.springframework.http.HttpStatus; + +//에러 코드 모음집 +//사용법 에러명("message", 실제 에러상태) +public enum ErrorCode { + + INVALID_PRESET_TITLE_LENGTH("프리셋 타이틀이 6자를 초과하거나 2자 미만일 수 없습니다.",HttpStatus.BAD_REQUEST), + USER_NOT_FOUND("존재하지 않은 유저입니다.", HttpStatus.NOT_FOUND), + GROUP_NOT_FOUND("존재하지 않는 그룹입니다.", HttpStatus.BAD_REQUEST), + GROUP_NAME_ALREADY_EXISTS("중복된 그룹 이름입니다.", HttpStatus.CONFLICT), + FAILED_CREATE_GROUP("최대 5개의 그룹만 생성할 수 있습니다.", HttpStatus.UNPROCESSABLE_ENTITY), + PRESET_NOT_FOUND("존재하지 않는 프리셋에 대한 작업입니다.", HttpStatus.NOT_FOUND), + FAILED_DELETE_PRESET_IN_GROUP("그룹에서 사용하는 프리셋은 삭제할 수 없습니다.", HttpStatus.BAD_REQUEST), + SUMMARY_NOT_FOUND("해당 요약 내용을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + MEMO_NOT_FOUND("해당 메모 내용을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + HIKING_NOT_FOUND("존재하지 않는 하이킹입니다.", HttpStatus.NOT_FOUND), + CHILD_ACCOUNT_HIKING_NOT_TERMINABLE("자식 계정은 하이킹 중 종료 할 수 없습니다.", HttpStatus.FORBIDDEN), + MAX_GPT_REQUEST_TODAY("하루에 GPT 요약 요청은 최대 3번까지 가능합니다.", HttpStatus.BAD_REQUEST), + GPT_BAD_REQUEST("GPT 요청을 처리하다가 실패했습니다.", HttpStatus.BAD_REQUEST), + FAILED_PROMPT_TO_JSON("요약을 위한 본문 처리에 실패했습니다. 잠시 후 다시 시도해주세요.", HttpStatus.BAD_REQUEST), + INVALID_USER_LOGIN_ID_FORMAT("유저 로그인 아이디 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), + INVALID_PASSWORD_FORMAT("비밀번호 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), + INVALID_NICKNAME_FORMAT("닉네임 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), + INVALID_EMAIL_FORMAT("이메일 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), + DUPLICATED_USER_LOGIN_ID("이미 존재하는 아이디입니다.", HttpStatus.CONFLICT), + DUPLICATED_EMAIL("이미 존재하는 이메일입니다.", HttpStatus.CONFLICT), + NOT_EQUAL_PASSWORD("비밀번호와 비밀번호 확인이 일치하지 않습니다.", HttpStatus.BAD_REQUEST), + FILE_NOT_FOUND("Default Block Website File을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + FAILED_CREATE_CHILD_USER("그룹에는 최대 30명의 자식 계정만 존재할 수 있습니다.", HttpStatus.UNPROCESSABLE_ENTITY), + TIMER_NOT_FOUND("존재하지 않는 타이머입니다.", HttpStatus.NOT_FOUND), + TIME_SLOT_OVERLAP("선택한 시간 범위가 다른 예약과 겹칩니다.", HttpStatus.CONFLICT), + FAIL_SEND_SSE_MESSAGE("메시지를 전송하는데 실패했습니다.", HttpStatus.BAD_REQUEST), + INVALID_LOGIN_PROCESS("등록되지 않은 아이디이거나 아이디 또는 비밀번호를 잘못 입력했습니다.", HttpStatus.NOT_FOUND), + UNAUTHORIZED("유효하지 않은 토큰입니다.", HttpStatus.UNAUTHORIZED), + DO_NOT_HAVE_USERGROUP("child 계정이지만 소속된 group이 존재하지 않습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + DO_NOT_HAVE_USER("존재하지 않는 유저입니다.", HttpStatus.INTERNAL_SERVER_ERROR), + DO_NOT_HAVE_GROUP("존재하지 않는 그룹입니다.", HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_ROOT_PASSWORD("비밀번호가 일치하지 않습니다.", HttpStatus.CONFLICT), + DUPLICATED_NICKNAME("현재와 동일한 닉네임입니다.", HttpStatus.CONFLICT), + DUPLICATED_PASSWORD("현재와 동일한 비밀번호입니다.", HttpStatus.CONFLICT), + FAILED_SEND_EMAIL("이메일을 전송하는데 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + EMAIL_CODE_NOT_FOUND("인증시간이 만료되었거나 인증코드를 발급받지 않으셨습니다. 다시 시도해주세요.", HttpStatus.NOT_FOUND), + NOT_EQUAL_EMAIL_CODE("인증 코드가 일치하지 않습니다.", HttpStatus.CONFLICT), + INVALID_EMAIL_AUTHENTICATION("이메일이 인증되지 않았습니다.", HttpStatus.FORBIDDEN), + SAME_EMAIL("기존과 동일한 이메일입니다.", HttpStatus.CONFLICT), + USER_LOGIN_ID_NOT_FOUND("존재하지 않는 아이디입니다.", HttpStatus.NOT_FOUND), + NOT_ROOT("서브 계정은 비밀번호 재발급 기능을 이용할 수 없습니다.", HttpStatus.FORBIDDEN), + NOT_EQUAL_EMAIL("아이디에 해당하는 이메일 주소가 아닙니다.", HttpStatus.CONFLICT), + SETTINGS_FOR_ROOT("서브 계정은 회원정보 관리 페이지에 접근할 수 없습니다.", HttpStatus.FORBIDDEN), + CHILD_USER_NOT_FOUND("자식 유저가 아니거나 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + DUPLICATED_URL("이미 존재하는 url 입니다.", HttpStatus.BAD_REQUEST) + ; + + private final String message; + private final HttpStatus httpStatus; + + ErrorCode(String message, HttpStatus httpStatus) { + this.message = message; + this.httpStatus = httpStatus; + } + + public String getMessage() { + return message; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorResponse.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorResponse.java index ec7d75da..25e2ec30 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorResponse.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/ErrorResponse.java @@ -1,3 +1,18 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:45d034444f22b0a72c9804b8c4f99638e6868272d2c5f088f2ed16a8363a30f0 -size 572 +package com.dinnertime.peaktime.global.exception; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +// 오류를 json으로 변환하여 처리하는 메서드 +public class ErrorResponse { + private static final ObjectMapper objectMapper = new ObjectMapper(); + private String errorMessage; + + public String convertToJson() throws JsonProcessingException { + return objectMapper.writeValueAsString(this); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/GlobalExceptionHandler.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/GlobalExceptionHandler.java index cb9cb36c..018b5b5a 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/GlobalExceptionHandler.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/exception/GlobalExceptionHandler.java @@ -1,3 +1,113 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e3eea3c0e1b4e3ec9da569cbb51121b48495e8b8a09e36bdfb4e77105b5ca2b -size 5029 +package com.dinnertime.peaktime.global.exception; + +import com.dinnertime.peaktime.global.util.ResultDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.time.format.DateTimeParseException; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(CustomException.class) + public ResponseEntity handleCustomException(final CustomException ex) { + ResultDto response = ResultDto.res( + ex.getErrorCode().getHttpStatus().value(), + ex.getMessage() + ); + + return new ResponseEntity<>(response, ex.getErrorCode().getHttpStatus()); + } + + // 회원 정보 불일치 + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentialsException(final BadCredentialsException ex) { + ResultDto response = ResultDto.res( + HttpStatus.NOT_FOUND.value(), + "등록되지 않은 아이디이거나 아이디 또는 비밀번호를 잘못 입력했습니다." + ); + + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + // 회원탈퇴한 유저로 로그인 시도시 + @ExceptionHandler(InternalAuthenticationServiceException.class) + public ResponseEntity handleInternalAuthenticationServiceException(final InternalAuthenticationServiceException ex) { + ResultDto response = ResultDto.res( + HttpStatus.NOT_FOUND.value(), + "등록되지 않은 아이디이거나 아이디 또는 비밀번호를 잘못 입력했습니다." + ); + + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + ResultDto response = ResultDto.res( + HttpStatus.BAD_REQUEST.value(), + // Annotation에 설정된 메시지 추출 후 담기 + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage() + ); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) { + ResultDto response = ResultDto.res( + HttpStatus.BAD_REQUEST.value(), + "잘못된 형식의 요청입니다." + ); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { + ResultDto response = ResultDto.res( + HttpStatus.FORBIDDEN.value(), + "해당 권한으로는 이 API를 호출할 수 없습니다." + ); + return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler({ MethodArgumentTypeMismatchException.class, DateTimeParseException.class }) + public ResponseEntity handleDateParsingException(Exception ex) { + log.info(ex.getMessage()); + ResultDto response = ResultDto.res( + HttpStatus.BAD_REQUEST.value(), + "유효하지 않은 날짜 형식입니다." + ); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(final RuntimeException ex) { + ResultDto response = ResultDto.res( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." + ); + + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(final Exception ex) { + log.info(ex.getMessage()); + ResultDto response = ResultDto.res( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." + ); + + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/AuthUtil.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/AuthUtil.java index 0b67231a..9a9e199e 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/AuthUtil.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/AuthUtil.java @@ -1,3 +1,43 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a081e254de469f76152cf93d6f9a03440e8f5e8748b9c3c597ea7f16dd368725 -size 2226 +package com.dinnertime.peaktime.global.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AuthUtil { + + // 아이디 형식 검사 (형식에 맞으면 true, 형식에 맞지 않으면 false) + public static boolean checkFormatValidationUserLoginId(String userLoginId) { + // 정규식: 영문과 숫자로 이루어진 5자 이상 15자 이하의 문자열 + String regex = "^[a-zA-Z0-9]{5,15}$"; + return userLoginId.matches(regex); + } + + // 영문 대문자를 영문 소문자로 변환 + public static String convertUpperToLower(String input) { + return input.toLowerCase(); + } + + // 비밀번호 형식 검사 (형식에 맞으면 true, 형식에 맞지 않으면 false) + public static boolean checkFormatValidationPassword(String password) { + // 정규식 : 영문 대문자, 영문 소문자, 숫자, 특수문자(모든 특수문자로 확장하여 허용)를 각각 포함하여 최소 8자 이상 -> 영문 대문자, 영문 소문자, 숫자, 특수문자를 제외한 문자는 false 처리. + String pattern = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>\\/?])[A-Za-z\\d!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>\\/?]{8,}$"; + // Pattern 및 Matcher를 사용해 정규식 검증 + Pattern compiledPattern = Pattern.compile(pattern); + Matcher matcher = compiledPattern.matcher(password); + // 정규식 패턴에 일치하면 true 반환, 아니면 false 반환 + return matcher.matches(); + } + + // 닉네임 형식 검사 (형식에 맞으면 true, 형식에 맞지 않으면 false) + public static boolean checkFormatValidationNickname(String nickname) { + // 정규식: 한글, 영문 대소문자, 숫자로 이루어진 2자 이상 8자 이하의 문자열 + String regex = "^[a-zA-Z0-9가-힣_\\[\\]]{2,15}$"; + return nickname.matches(regex); + } + + // 이메일 형식 검사 (형식에 맞으면 true, 형식에 맞지 않으면 false) + public static boolean checkFormatValidationEmail(String email) { + String regex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; + return email.matches(regex); + } +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/CommonSwaggerResponse.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/CommonSwaggerResponse.java index d71a2ead..b3e7799a 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/CommonSwaggerResponse.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/CommonSwaggerResponse.java @@ -1,3 +1,30 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:28f14da93b9cac18eea30c954dc5283f294e9c758803f30826d18443c1e1917a -size 1477 +package com.dinnertime.peaktime.global.util; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import java.lang.annotation.*; + +public class CommonSwaggerResponse { + + @Inherited + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "조회할 데이터가 없습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 페이지입니다.", + content = @Content(schema = @Schema(implementation = ResultDto.class))), + }) + + public @interface CommonResponses{} + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/EmailService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/EmailService.java index 72db5a88..53cd15ee 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/EmailService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/EmailService.java @@ -1,3 +1,66 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b08da7b823e1994c979715876130e1e985545789d02e6c2cffe7f0a6f71237e -size 2534 +package com.dinnertime.peaktime.global.util; + +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.EmailServerException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmailService { + + @Value("${spring.mail.tx}") + private String tx; + + private final JavaMailSender javaMailSender; + + // 랜덤 인증 코드 보내기 + @Async + public void sendCode(String rx, String code) { + LocalDateTime expirationTime = LocalDateTime.now().plusMinutes(3); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH:mm"); + String formattedExpirationTime = expirationTime.format(formatter); + + String title = "Peaktime 인증 코드 메일입니다!"; + String content = "이메일을 인증하기 위한 절차입니다." + + "

" + "인증 코드는 " + code + "입니다." + + "

" + "인증 코드는 " + formattedExpirationTime + "까지 유효합니다."; + + this.send(rx, title, content); + } + + // 랜덤 비밀번호 보내기 + @Async + public void sendPassword(String rx, String password) { + String title = "Peaktime 임시 비밀번호 발급 메일입니다!"; + String content = "회원님의 임시 비밀번호는 " + password + "입니다."; + this.send(rx, title, content); + } + + // 실제로 전송하는 메서드 + private void send(String rx, String title, String content) { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8"); + helper.setFrom(this.tx); + helper.setTo(rx); + helper.setSubject(title); + helper.setText(content, true); + javaMailSender.send(mimeMessage); + } catch (Exception e) { + throw new CustomException(ErrorCode.FAILED_SEND_EMAIL); + } + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/RedisService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/RedisService.java index 7e28ae19..76b77295 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/RedisService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/RedisService.java @@ -1,3 +1,258 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:572a93c2dbd6b9a96d244fbfd72f5447b07f9a9a5a5415040d78e1784fae7126 -size 10197 +package com.dinnertime.peaktime.global.util; + +import com.dinnertime.peaktime.domain.schedule.entity.Schedule; +import com.dinnertime.peaktime.domain.schedule.service.dto.RedisSchedule; +import com.dinnertime.peaktime.domain.timer.entity.Timer; +import com.dinnertime.peaktime.domain.timer.service.dto.request.TimerCreateRequestDto; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Service; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RedisService { + + private final RedisTemplate stringRedisTemplate; + private final RedisTemplate scheduleRedisTemplate; + private final RedisTemplate redisTemplate; + private static final int TTL = 24; // 하루 단위 gpt + private static final int DAY = 7; + private static final int DAY_MINUTE = 1440; + private static final int MAX_ATTENTION_TIME = 240; + + public void saveRefreshToken(long userId, String refreshToken) { + String key = "refreshToken:" + userId; + redisTemplate.opsForValue().set(key, refreshToken); + } + + public String getRefreshToken(long userId) { + String key = "refreshToken:" + userId; + return (String) redisTemplate.opsForValue().get(key); + } + + public void removeRefreshToken(long userId) { + String key = "refreshToken:" + userId; + redisTemplate.delete(key); + } + + public void saveEmailCode(String lowerEmail, String code) { + // 이미 이메일이 소문자로 변환되었다고 가정 + String key = "emailCode:" + lowerEmail; + redisTemplate.opsForValue().set(key, code); + redisTemplate.expire(key, 180, TimeUnit.SECONDS); + } + + public String getEmailCode(String lowerEmail) { + // 이미 이메일이 소문자로 변환되었다고 가정 + String key = "emailCode:" + lowerEmail; + return (String) redisTemplate.opsForValue().get(key); + } + + public void removeEmailCode(String lowerEmail) { + // 이미 이메일이 소문자로 변환되었다고 가정 + String key = "emailCode:" + lowerEmail; + redisTemplate.delete(key); + } + + public void saveEmailAuthentication(String lowerEmail) { + // 이미 이메일이 소문자로 변환되었다고 가정 + String key = "emailAuthentication:" + lowerEmail; + redisTemplate.opsForValue().set(key, "Authenticated"); + } + + public String getEmailAuthentication(String lowerEmail) { + // 이미 이메일이 소문자로 변환되었다고 가정 + String key = "emailAuthentication:" + lowerEmail; + return (String) redisTemplate.opsForValue().get(key); + } + + public void removeEmailAuthentication(String lowerEmail) { + // 이미 이메일이 소문자로 변환되었다고 가정 + String key = "emailAuthentication:" + lowerEmail; + redisTemplate.delete(key); + } + + public Integer getGPTcount(Long userId) { + String key = "gpt_usage_count:" + userId; + Integer count = (Integer) redisTemplate.opsForValue().get(key); + return count; + } + + public void setGPTIncrement(Long userId) { + String key = "gpt_usage_count:" + userId; + // 초기 설정 or 개수 증가 + redisTemplate.opsForValue().increment(key); + if (getGPTcount(userId) == 1) { // 맨 처음 생성 시 expire 설정 + redisTemplate.expire(key, TTL, TimeUnit.HOURS); + } + } + + public boolean checkTimerByGroupId(Long groupId, int start, int end) { + //키는 timer:그룹아이디 + String key = "timer:"+groupId; + + ZSetOperations zSet = stringRedisTemplate.opsForZSet(); + + //key에 해당하는 score(start시간) 중 겹치는 것을 확인 + //최대 4시간 집중할 수 있으므로 시작시간 - 240부터 end시간까지 중 시작시간이 것을 확인 + // + Set checkRange = zSet.rangeByScore(key, start-MAX_ATTENTION_TIME, end); + + // 겹치는지 검사 + //한개라도 겹치면 true + //겹치면 true + return checkRange != null && checkRange.stream().anyMatch(range -> { + String[] parts = range.split("-"); + int existingStart = Integer.parseInt(parts[0]); + int existingEnd = Integer.parseInt(parts[1]); + + log.info(existingStart + "-" + existingEnd); + + return (start < existingEnd && end > existingStart); // 겹치는지 조건 확인 + }); + + } + + public void deleteTimerByTimer(Long groupId, int start, int end) { + String key = "timer:"+groupId; + ZSetOperations zSet = stringRedisTemplate.opsForZSet(); + log.info("타이머 제거 "+key+" "+start); + + zSet.remove(key, start +"-"+end+"-"+groupId, String.valueOf(start)); + } + + public void deleteTimerByTimer(Timer timer) { + Long groupId = timer.getGroup().getGroupId(); + + String key = "timer:"+groupId; + ZSetOperations zSet = stringRedisTemplate.opsForZSet(); + //레디스에서 타이머 삭제 + int repeatDay = timer.getRepeatDay(); + LocalDateTime startTime = timer.getStartTime(); + int attentionTime = timer.getAttentionTime(); + int plusMinute = (startTime.getHour()*60) + startTime.getMinute(); + + IntStream.range(0, DAY) + //반복 요일 필터링 + .filter(i -> (repeatDay & (1 << i)) != 0) + .forEach(i -> { + int start = DAY_MINUTE * i + plusMinute; + int end = start + attentionTime; + zSet.remove(key, start +"-"+end+"-"+groupId+"-"+ timer.getTimerId(), String.valueOf(start)); + log.info("타이머 삭제: {}", key); + }); + } + + //현재 시간을 가져와서 보내주기 + public List findTimerByStart(int start) { + + // "timer:"로 시작하는 모든 키 가져오기 + ZSetOperations zSet = stringRedisTemplate.opsForZSet(); + Set keys = stringRedisTemplate.keys("timer:*"); + + // 각 키에서 해당 score에 해당하는 요소만 가져오기 + Set elements = new HashSet<>(); + for (String key : keys) { + Set matchedElements = zSet.rangeByScore(key, start, start); + elements.addAll(matchedElements); + } + + if(elements.isEmpty()) { + return null; + } + + return elements.stream().toList(); + } + + public void addSchedule(Schedule schedule) { + String key = "schedule:" + LocalDate.now(); + log.info("오늘 스케줄 추가: {}", key); + + //레디스에 저장할 객체 생성 + RedisSchedule saveSchedule = RedisSchedule.createRedisSchedule(schedule); + + ListOperations listOps = scheduleRedisTemplate.opsForList(); + Long ttl = scheduleRedisTemplate.getExpire(key); + + listOps.rightPush(key, saveSchedule); + + // 만료 시간 설정 + if (ttl == null || ttl <= 0) { + LocalDateTime midnight = LocalDate.now().plusDays(1).atStartOfDay(); + ttl = Duration.between(LocalDateTime.now(), midnight).getSeconds(); + scheduleRedisTemplate.expire(key, ttl, TimeUnit.SECONDS); + } + } + + public void addFirstSchedule(List scheduleList) { + String key = "schedule:" + LocalDate.now(); + + ListOperations listOps = scheduleRedisTemplate.opsForList(); + listOps.rightPushAll(key, scheduleList); + + // 자정까지 남은 시간으로 만료 설정 + LocalDateTime midnight = LocalDate.now().plusDays(1).atStartOfDay(); + long ttl = Duration.between(LocalDateTime.now(), midnight).getSeconds(); + scheduleRedisTemplate.expire(key, ttl, TimeUnit.SECONDS); + + log.info("첫 스케줄 추가 완료: {}", key); + } + + public void addTimerList(Timer timer, int repeatDay, int plusMinute, int attentionTime) { + Long groupId = timer.getGroup().getGroupId(); + + String key = "timer:" + groupId; + ZSetOperations zSet = stringRedisTemplate.opsForZSet(); + + IntStream.range(0, DAY) + //반복 요일 필터링 + .filter(i -> (repeatDay & (1 << i)) != 0) + .forEach(i -> { + int start = DAY_MINUTE * i + plusMinute; + int end = start + attentionTime; + zSet.add(key, start + "-" + end + "-" + groupId+"-"+timer.getTimerId(), start); + log.info("타이머 추가: {}", key); + }); + } + + public void deleteScheduleByTimer(Timer timer) { + String key = "schedule:" + LocalDate.now(); + log.info("오늘 스케줄 삭제: {}", key); + + ListOperations listOps = scheduleRedisTemplate.opsForList(); + + List existingSchedules = listOps.range(key, 0, -1); + + if(existingSchedules==null) return; + + // 특정 타이머 ID가 아닌 항목만 필터링 + List filteredSchedules = existingSchedules.stream() + .filter(redisSchedule -> !Objects.equals(redisSchedule.getTimerId(), timer.getTimerId())) + .collect(Collectors.toList()); + // 기존 키 삭제 + scheduleRedisTemplate.delete(key); + + // 필터링된 스케줄 다시 저장 + if (!filteredSchedules.isEmpty()) { + listOps.rightPushAll(key, filteredSchedules); + } + } +} \ No newline at end of file diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/ResultDto.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/ResultDto.java index 10d2b7fe..5b204a15 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/ResultDto.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/ResultDto.java @@ -1,3 +1,38 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a0ec7be08bba6a8fff9984031723af083ac56421be9ebf43c1eb5b553884858 -size 883 +package com.dinnertime.peaktime.global.util; + +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class ResultDto { + + private int status; + + private String message; + + private T data; + + public ResultDto(final int status, final String message) { + this.status = status; + this.message = message; + data = null; + } + + // data 비포함, 상태코드와 메시지만 + public static ResultDto res(final int status, final String message) { + return res(status, message, null); + } + + // data 포함해서 전송 + public static ResultDto res(final int status, final String message, final T t) { + return ResultDto.builder() + .data(t) + .status(status) + .message(message) + .build(); + + } + +} diff --git a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/chatgpt/ChatGPTService.java b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/chatgpt/ChatGPTService.java index f3453aea..ed2d6017 100644 --- a/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/chatgpt/ChatGPTService.java +++ b/BE/peaktime/src/main/java/com/dinnertime/peaktime/global/util/chatgpt/ChatGPTService.java @@ -1,3 +1,134 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:788cc0a8cfd380649d4002c2725b045e594b65e74cc6038806311638e4562e84 -size 5809 +package com.dinnertime.peaktime.global.util.chatgpt; + +import com.dinnertime.peaktime.domain.summary.service.dto.request.SaveSummaryRequestDto; +import com.dinnertime.peaktime.global.exception.CustomException; +import com.dinnertime.peaktime.global.exception.ErrorCode; +import com.dinnertime.peaktime.global.util.RedisService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + + +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ChatGPTService { + + private static final int MAX_TOKEN = 1000; + private static final int MAX_GPT_REQUEST_PER_DAY = 3; + private final RedisService redisService; + + @Value("${openai.api.model}") + private String gptModel; + + @Value("${openai.api.key}") + private String apiKey; + + @Value("${openai.api.url}") + private String apiURL; + + public String getGPTResult(SaveSummaryRequestDto requestDto, Long userId) { + + // 1. 유저당 하루 gpt 사용 횟수(3번) 처리하기 + + Integer count = redisService.getGPTcount(userId); + count = count == null ? 0 : count; // 초기 횟수가 null이면 0으로 처리 + + // 유저마다 하루 gpt 요청 최대 3번 + if (count >= MAX_GPT_REQUEST_PER_DAY) { + throw new CustomException(ErrorCode.MAX_GPT_REQUEST_TODAY); + } + + // 요청 본문 준비 + // 모든 형태는 hashmap 형태로 담아서 처리한다. + // 1. requestBody 요청 부분에 Model 설정하기 + RestTemplate restTemplate = new RestTemplate(); + Map requestBody = new HashMap<>(); + requestBody.put("model", gptModel); + + // 2. message 부분에 role, content 형식의 역할 부여 추가해야 함 + // role : 모델의 초기 성격이나 반응을 처음으로 생성할 때 system, 질의 응답을 하는 user, 응답을 받은 내용을 처리하는 assistant + // 한글로 작성해도 되지만, 명확한 의미 부여를 위해 영어로 작성하는 것으로 권장됨 + + // system 초기 설정 + String systemContent = "You are a helpful assistant specialized in summarizing texts. When additional keywords are provided, include related information in the summary."; + Map systemMessage = new HashMap<>(); + + systemMessage.put("role", "system"); + systemMessage.put("content", systemContent); + + + // description으로 받아오는 문자열 추가 설정도 가능 + + // 3. 추가 키워드 받아와서 프롬포트 문자열 생성하기 + + // user : 질의를 진행하는 실제 템플릿 양식(질문 양식 작성) + Map userMessage = new HashMap<>(); + + try { + String originContent = requestDto.getContent(); + + // ObjectMapper를 사용하여 JSON 객체 생성 + ObjectMapper mapper = new ObjectMapper(); + ObjectNode json = mapper.createObjectNode(); + + // JSON 노드에 원본 텍스트 추가 (줄바꿈 포함) + json.put("text", originContent); + + // JSON 문자열 출력 (자동으로 이스케이프 처리됨) + String jsonString = mapper.writeValueAsString(json); + + + String userContent = "Please summarize the following text, incorporating the additional keywords provided even if they are not explicitly mentioned in the text. If the keywords are absent, provide relevant information based on related context.\n\nText to summarize:\n" + + jsonString + "\n\nAdditional keywords: " + Arrays.toString(requestDto.getKeywords()) + "\n\n그리고 한글로 작성해줘."; + log.info(userContent); + userMessage.put("role", "user"); + userMessage.put("content", userContent); + } catch (Exception e) { + throw new CustomException(ErrorCode.FAILED_PROMPT_TO_JSON); + } + + // role system, user binding해서 requestBody에 추가하기 + requestBody.put("messages", List.of(systemMessage, userMessage)); + requestBody.put("max_tokens", MAX_TOKEN); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + apiKey); + headers.set("Content-Type", "application/json"); + + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + try { + // post 요청으로 보내기 위해 restTemplate 활용 + ResponseEntity response = restTemplate.exchange(apiURL, HttpMethod.POST, requestEntity, JsonNode.class); + log.info("GPT STATUS CODE는? " + response.getStatusCode()); + if (response.getStatusCode() == HttpStatus.OK) { + // response 받아서 처리하기 + JsonNode responseBody = response.getBody(); + log.info(Objects.requireNonNull(response.getBody()).toString()); + // 응답 내용 추출하기 + JsonNode messageNode = responseBody.path("choices").get(0).path("message").path("content"); + String text = messageNode.asText(); + + // redis count 늘리기 + redisService.setGPTIncrement(userId); + return text; + } + } catch (Exception e) { + throw new CustomException(ErrorCode.GPT_BAD_REQUEST); + } + return ""; + + } +} + diff --git a/BE/peaktime/src/main/resources/DistractionsWebsites.txt b/BE/peaktime/src/main/resources/DistractionsWebsites.txt index 309d30fa..61ac681c 100644 --- a/BE/peaktime/src/main/resources/DistractionsWebsites.txt +++ b/BE/peaktime/src/main/resources/DistractionsWebsites.txt @@ -1,3 +1,75 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c47d8ad527888774fe69e8f43c7964acf71834da4e3aedecebec872ba922d47 -size 927 +9to5mac.com +arstechnica.com +bilibili.com +bleacherreport.com +boingboing.net +break.com +businessinsider.com +buzzfeed.com +cbssports.com +collegehumor.com +cracked.com +deviantart.com +digg.com +discord.com +engadget.com +espn.com +facebook.com +fark.com +foxsports.com +funnyordie.com +gizmodo.com +hulu.com +imgur.com +instagram.com +itemfix.com +lifehacker.com +macrumors.com +mashable.com +metafilter.com +miniclip.com +mlb.com +myspace.com +nba.com +netflix.com +news.ycombinator.com +nfl.com +nhl.com +pinterest.com +pornhub.com +producthunt.com +qzone.qq.com +readwrite.com +recode.net +reddit.com +roblox.com +slashdot.org +snapchat.com +tagged.com +techcrunch.com +techmeme.com +ted.com +thenextweb.com +theoatmeal.com +theonion.com +theverge.com +tieba.baidu.com +tiktok.com +tmz.com +tomshardware.com +tumblr.com +twitch.tv +twitter.com +venturebeat.com +vimeo.com +vk.com +weibo.com +whatsapp.com +wired.com +xhamster.com +xkcd.com +xnxx.com +xvideos.com +youtube.com +yy.com +zdnet.com \ No newline at end of file diff --git a/BE/peaktime/src/test/java/com/dinnertime/peaktime/chatgpt/ChatGPTServiceTest.java b/BE/peaktime/src/test/java/com/dinnertime/peaktime/chatgpt/ChatGPTServiceTest.java index b04e23d5..12d4018c 100644 --- a/BE/peaktime/src/test/java/com/dinnertime/peaktime/chatgpt/ChatGPTServiceTest.java +++ b/BE/peaktime/src/test/java/com/dinnertime/peaktime/chatgpt/ChatGPTServiceTest.java @@ -1,3 +1,50 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d6d9eb72a5a2d0b0a6e93907bfb91cce696cced3676377fce596bf2f1cd9a3ff -size 1564 +package com.dinnertime.peaktime.chatgpt; + +import com.dinnertime.peaktime.global.util.chatgpt.ChatGPTRequest; +import com.dinnertime.peaktime.global.util.chatgpt.ChatGPTResponse; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest +@Slf4j +public class ChatGPTServiceTest { + @Value("${openai.api.model}") + private String model; + + @Value("${openai.api.url}") + private String apiURL; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + @Qualifier("redisTemplate") + private RedisTemplate redisTemplate; + + ChatGPTServiceTest(@Qualifier("openAiRestTemplate") RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Test + public void test() { + String prompt = "안녕하세요."; + ChatGPTRequest request = new ChatGPTRequest(model, prompt); + ChatGPTResponse chatGPTResponse = restTemplate.postForObject(apiURL, request, ChatGPTResponse.class); + + log.info(chatGPTResponse.getChoices().get(0).getMessage().getContent()); + + } + + @Test + public void redisTest() { + String key = "rooting:" + 1; + redisTemplate.opsForValue().set(key, "dsfsd"); + } + +}