diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..d5a2c42 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,68 @@ +name: Java CI/CD with Gradle # 워크플로우 이름 + +on: # 언제 실행할 것인지에 대한 설정 + push: # 푸시 이벤트가 발생할 때 + branches: [ "main" ] # main 브랜치에 푸시 이벤트가 발생할 때 + +permissions: # 권한 설정 + contents: read # 해당 레포지토리의 모든 파일을 읽기 권한을 부여한다. + +jobs: # 실행할 작업들에 대한 설정 + # Spring Boot 애플리케이션을 빌드하여 도커허브에 푸시하는 과정 + build-docker-image: # 작업 이름 - 애플리케이션을 빌드하여 도커허브에 푸시하는 작업 + runs-on: ubuntu-latest # 실행 환경 설정 + steps: # 작업 내용 + - uses: actions/checkout@v3 + + - name: Grant execute permission for gradlew # gradlew 파일에 실행 권한 부여 + run: chmod +x gradlew # gradlew 파일에 실행 권한을 부여한다. + + - name: Set up JDK 17 # 1. Java 17 세팅 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: make application.properties # 2. application.properties 파일 생성 + run: | + mkdir -p ./src/main/resources + cd ./src/main/resources + touch ./application.properties + echo "${{ secrets.PROPERTIES }}" > ./application.properties + shell: bash + + - name: Build with Gradle # 3. Spring Boot 애플리케이션 빌드, jar 파일 생성 + uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 + with: + arguments: clean bootJar + + - name: docker image build # 4. Docker 이미지 빌드 + run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE }} . + + - name: docker login # 5. DockerHub 로그인 + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: docker Hub push # 6. Docker Hub 이미지 푸시 + run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE }} + + deploy: # 작업 이름 - 도커허브에 푸시한 이미지를 서버에 배포하는 작업 + needs: build-docker-image # build-docker-image 작업이 성공적으로 완료되어야 함 + runs-on: ubuntu-latest # 실행 환경 설정 + steps: # 작업 내용 + - name: ssh connect & production # EC2에 접속하여 도커 이미지 실행 + uses: appleboy/ssh-action@master + with: + host: ${{secrets.HOST}} # EC2 인스턴스의 IP 주소 + username: ${{secrets.USERNAME}} # EC2 인스턴스의 사용자 이름 (ubuntu) + key: ${{secrets.PASSWORD}} # EC2 인스턴스의 비밀번호 (SSH 키) + script: | # 실행할 스크립트 + sudo docker login --username ${{secrets.DOCKERHUB_USERNAME}} --password ${{secrets.DOCKERHUB_PASSWORD}} + sudo docker pull ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE}} + sudo docker ps -q | xargs -r sudo docker stop + sudo docker ps -aq | xargs -r sudo docker rm + sudo docker run --name redis --rm -d -p 6379:6379 redis + sudo docker run --name ${{secrets.DOCKERHUB_IMAGE}} --rm -d -p 8080:8080 ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE}} + sudo docker system prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d61011e --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +HELP.md +.gradle +build/ +!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 +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### IntelliJ IDEA ### +application.properties \ No newline at end of file diff --git a/.gitmessage.txt b/.gitmessage.txt new file mode 100644 index 0000000..bb2c529 --- /dev/null +++ b/.gitmessage.txt @@ -0,0 +1,50 @@ +################ +# <타입>: <제목> - <이슈번호> 의 형식으로 제목을 아래 공백줄에 작성 +# 제목은 50자 이내 / 변경사항이 "무엇"인지 명확히 작성 / 끝에 마침표 금지 +# 예) feat: 로그인 기능 추가 - #2 +# 바로 아래 공백은 제목과 본문의 분리를 위함 +################ +# 본문(구체적인 내용)을 아랫줄에 작성 +# 여러 줄의 메시지를 작성할 땐 "-"로 구분 (한 줄은 72자 이내) +################ +#feat +#새로운 기능 추가 +# +#fix +#버그 수정 +# +#design +#CSS 등 사용자 UI 디자인 변경 +# +#!BREAKING CHANGE +#커다란 API 변경의 경우 +# +#!HOTFIX +#급하게 치명적인 버그를 고쳐야하는 경우 +# +#style +#코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우 +# +#refactor +#프로덕션 코드 리팩토링 +# +#comment +#필요한 주석 추가 및 변경 +# +#docs +#문서 수정 +# +#test +#테스트 추가, 테스트 리팩토링(프로덕션 코드 변경 X) +# +#setting +#패키지 설치, 개발 설정 +# +#chore +#빌드 테스트 업데이트, 패키지 매니저를 설정하는 경우(프로덕션 코드 변경 X) +# +#rename +#파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우 +# +#remove +#파일을 삭제하는 작업만 수행한 경우 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..76171a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:17-jdk +ARG JAR_FILE=build/libs/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cbbca56 --- /dev/null +++ b/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.1' + id 'io.spring.dependency-management' version '1.1.5' +} + +group = 'com.hackathonteam1' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + + testCompileOnly 'org.projectlombok:lombok:1.18.32' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' + + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'org.springframework.boot:spring-boot-starter-data-jpa'//JPA + implementation 'org.springframework.boot:spring-boot-starter-jdbc'//JDBC + runtimeOnly 'com.mysql:mysql-connector-j'//MySQL + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5'//JWT + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'//JWT + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'//JWT + implementation 'javax.xml.bind:jaxb-api:2.3.1'//JAXB + + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'//AWS + + implementation 'org.springframework.boot:spring-boot-starter-data-redis'//Redis +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..b740cf1 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0d73b8d --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'refreshrator' diff --git a/src/main/java/com/hackathonteam1/refreshrator/RefreshratorApplication.java b/src/main/java/com/hackathonteam1/refreshrator/RefreshratorApplication.java new file mode 100644 index 0000000..3ea5522 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/RefreshratorApplication.java @@ -0,0 +1,15 @@ +package com.hackathonteam1.refreshrator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@SpringBootApplication +@EnableJpaAuditing +public class RefreshratorApplication { + + public static void main(String[] args) { + SpringApplication.run(RefreshratorApplication.class, args); + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/annotation/AuthenticatedUser.java b/src/main/java/com/hackathonteam1/refreshrator/annotation/AuthenticatedUser.java new file mode 100644 index 0000000..0ede310 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/annotation/AuthenticatedUser.java @@ -0,0 +1,12 @@ +package com.hackathonteam1.refreshrator.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthenticatedUser { +} + diff --git a/src/main/java/com/hackathonteam1/refreshrator/annotation/resolver/AuthenticatedUserArgumentResolver.java b/src/main/java/com/hackathonteam1/refreshrator/annotation/resolver/AuthenticatedUserArgumentResolver.java new file mode 100644 index 0000000..857825e --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/annotation/resolver/AuthenticatedUserArgumentResolver.java @@ -0,0 +1,34 @@ +package com.hackathonteam1.refreshrator.annotation.resolver; + +import com.hackathonteam1.refreshrator.annotation.AuthenticatedUser; +import com.hackathonteam1.refreshrator.authentication.AuthenticationContext; +import com.hackathonteam1.refreshrator.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticatedUserArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthenticationContext authenticationContext; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticatedUser.class); + } + + //supportsParameter가 true를 반환할 때 호출 + @Override + public User resolveArgument(final MethodParameter parameter, //메서드의 파라미터를 나타내는 객체 + final ModelAndViewContainer mavContainer, //현재 요청 모델의 뷰 정보를 담고 있음 + final NativeWebRequest webRequest, //HTTP요청을 추상화한 객체 + final WebDataBinderFactory binderFactory) { //데이터바인더를 반환하는 팩토리 + return authenticationContext.getPrincipal(); //이제 인증된 User를 반환 + } +} + diff --git a/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationContext.java b/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationContext.java new file mode 100644 index 0000000..bb32065 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationContext.java @@ -0,0 +1,15 @@ +package com.hackathonteam1.refreshrator.authentication; + +import com.hackathonteam1.refreshrator.entity.User; +import lombok.Getter; +import lombok.Setter; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Setter +@Getter +@Component +@RequestScope +public class AuthenticationContext { + private User principal; +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationExtractor.java b/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationExtractor.java new file mode 100644 index 0000000..8b141cd --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationExtractor.java @@ -0,0 +1,24 @@ +package com.hackathonteam1.refreshrator.authentication; + +import com.hackathonteam1.refreshrator.exception.UnauthorizedException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; + +public class AuthenticationExtractor { + public static final String TOKEN_COOKIE_NAME = "AccessToken"; + + public static String extract(final HttpServletRequest request) { + + Cookie[] cookies = request.getCookies(); + + if (cookies != null) { + for (Cookie cookie : cookies) { + if (TOKEN_COOKIE_NAME.equals(cookie.getName())) { + return JwtEncoder.decodeJwtBearerToken(cookie.getValue()); //디코딩 + } + } + } + throw new UnauthorizedException(ErrorCode.COOKIE_NOT_FOUND); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationInterceptor.java b/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationInterceptor.java new file mode 100644 index 0000000..b98d908 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/authentication/AuthenticationInterceptor.java @@ -0,0 +1,47 @@ +package com.hackathonteam1.refreshrator.authentication; + +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.exception.NotFoundException; +import com.hackathonteam1.refreshrator.exception.UnauthorizedException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import com.hackathonteam1.refreshrator.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AuthenticationInterceptor implements HandlerInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + private final AuthenticationContext authenticationContext; + private final UserRepository userRepository; + + @Override + public boolean preHandle(final HttpServletRequest request, + final HttpServletResponse response, + final Object handler) { + if(request.getMethod().equals("OPTIONS")){ + return true; + } + if(request.getMethod().equals("GET") && (request.getRequestURI().equals("/recipes") || request.getRequestURI().matches("^/recipes/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"))){ + return true; + } + String accessToken = AuthenticationExtractor.extract(request); + UUID userId = UUID.fromString(jwtTokenProvider.getPayload(accessToken)); + User user = findExistingUser(userId); + authenticationContext.setPrincipal(user); + return true; + } + + private User findExistingUser(final UUID userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new UnauthorizedException(ErrorCode.INVALID_TOKEN)); + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/authentication/JwtEncoder.java b/src/main/java/com/hackathonteam1/refreshrator/authentication/JwtEncoder.java new file mode 100644 index 0000000..d664e56 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/authentication/JwtEncoder.java @@ -0,0 +1,30 @@ +package com.hackathonteam1.refreshrator.authentication; + +import com.hackathonteam1.refreshrator.exception.UnauthorizedException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import org.springframework.stereotype.Component; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Component +public class JwtEncoder { + public static final String TOKEN_TYPE="Bearer "; + + public static String encodeJwtToken(String token){ + String cookieValue = TOKEN_TYPE+token; + return URLEncoder.encode(cookieValue, StandardCharsets.UTF_8).replace("+", "%20"); + } + + public static String decodeJwtBearerToken(String cookieValue){ + String decodedValue = URLDecoder.decode(cookieValue,StandardCharsets.UTF_8); + + if (decodedValue.startsWith(TOKEN_TYPE)) { + return decodedValue.substring(TOKEN_TYPE.length()); + } + + throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); + } +} + diff --git a/src/main/java/com/hackathonteam1/refreshrator/authentication/JwtTokenProvider.java b/src/main/java/com/hackathonteam1/refreshrator/authentication/JwtTokenProvider.java new file mode 100644 index 0000000..ebca850 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/authentication/JwtTokenProvider.java @@ -0,0 +1,56 @@ +package com.hackathonteam1.refreshrator.authentication; + + +import com.hackathonteam1.refreshrator.exception.UnauthorizedException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final SecretKey key; + private final long validityInMilliseconds; + + //생성자 + public JwtTokenProvider(@Value("${security.jwt.token.secret-key}") final String secretKey, + @Value("${security.jwt.token.expire-length}") final long validityInMilliseconds) { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.validityInMilliseconds = validityInMilliseconds; + } + + //로그인시 토큰을 발급함 + public String createToken(final String payload) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setSubject(payload) //userid + .setIssuedAt(now) //발급 시간 + .setExpiration(expiration) //만료 시간 + .signWith(key, SignatureAlgorithm.HS256) //서명 + .compact(); //문자열로 반환 + } + + //페이로드 분석,userId 반환 + public String getPayload(final String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) //키 설정 + .build() + .parseClaimsJws(token)//parse:분석하다, 여기서 토큰이 유효하지 않으면 JwtException이 발생 + .getBody() + .getSubject(); + } catch (JwtException e) { + throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/authentication/PasswordHashEncryption.java b/src/main/java/com/hackathonteam1/refreshrator/authentication/PasswordHashEncryption.java new file mode 100644 index 0000000..cd469b0 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/authentication/PasswordHashEncryption.java @@ -0,0 +1,47 @@ +package com.hackathonteam1.refreshrator.authentication; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; + +@Component +public class PasswordHashEncryption { + + private static final String PBKDF2_WITH_SHA1 = "PBKDF2WithHmacSHA1"; + + private final String salt;// salt는 암호화할 때 사용하는 임의의 값 + private final int iterationCount;// 반복 횟수(암호화 강도) + private final int keyLength;// 키 길이(암호화된 비밀번호 길이) + + public PasswordHashEncryption(@Value("${encryption.pbkdf2.salt}") final String salt, + @Value("${encryption.pbkdf2.iteration-count}") final int iterationCount, + @Value("${encryption.pbkdf2.key-length}") final int keyLength) { + this.salt = salt; + this.iterationCount = iterationCount; + this.keyLength = keyLength; + } + + public String encrypt(final String plainPassword) { + try { + KeySpec spec = new PBEKeySpec(plainPassword.toCharArray(), salt.getBytes(), iterationCount, keyLength); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBKDF2_WITH_SHA1); + byte[] encodedPassword = keyFactory.generateSecret(spec) + .getEncoded(); + return Base64.getEncoder() + .withoutPadding() + .encodeToString(encodedPassword); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException("Cannot encrypt password"); + } + } + + public boolean matches(final String plainPassword, final String hashedPassword) { + return encrypt(plainPassword).equals(hashedPassword); + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/config/AuthenticationConfig.java b/src/main/java/com/hackathonteam1/refreshrator/config/AuthenticationConfig.java new file mode 100644 index 0000000..4e988c5 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/config/AuthenticationConfig.java @@ -0,0 +1,37 @@ +package com.hackathonteam1.refreshrator.config; + +import com.hackathonteam1.refreshrator.annotation.resolver.AuthenticatedUserArgumentResolver; +import com.hackathonteam1.refreshrator.authentication.AuthenticationInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class AuthenticationConfig implements WebMvcConfigurer { + + private final AuthenticationInterceptor authenticationInterceptor; + private final AuthenticatedUserArgumentResolver authenticatedUserArgumentResolver; + + private static final String[] ADD_PATH_PATTERNS = {"/fridge/**","/recipes/**","/auth/leave","/auth/logout", "/auth/likes", "/users/me/**"}; + private static final String[] EXCLUDE_PATH_PATTERNS = {"/auth/signin", "/auth/login", "/auth/refresh"}; + + //인터셉터 등록 + 경로 설정 + @Override + public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(authenticationInterceptor) + .addPathPatterns(ADD_PATH_PATTERNS) + .excludePathPatterns(EXCLUDE_PATH_PATTERNS); + + } + + //컨트롤러 메서드 파라미터에 인증된 유저가 들어가도록 함 + @Override + public void addArgumentResolvers(final List resolvers) { + resolvers.add(authenticatedUserArgumentResolver); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/config/CorsConfig.java b/src/main/java/com/hackathonteam1/refreshrator/config/CorsConfig.java new file mode 100644 index 0000000..493e03a --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/config/CorsConfig.java @@ -0,0 +1,29 @@ +package com.hackathonteam1.refreshrator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + private final long MAX_AGE_SECS = 3600; + @Value("${client.host}") + private List clientHosts; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(clientHosts.toArray(new String[0])) + .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), + HttpMethod.PATCH.name(), HttpMethod.DELETE.name(), + HttpMethod.OPTIONS.name()) + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(MAX_AGE_SECS); + } +} + diff --git a/src/main/java/com/hackathonteam1/refreshrator/config/RedisConfig.java b/src/main/java/com/hackathonteam1/refreshrator/config/RedisConfig.java new file mode 100644 index 0000000..4aced21 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/config/RedisConfig.java @@ -0,0 +1,42 @@ +package com.hackathonteam1.refreshrator.config; + +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.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + private final String redisHost; + private final int redisPort; + + public RedisConfig(@Value("${spring.data.redis.host}") String redisHost, + @Value("${spring.data.redis.port}")int redisPort){ + this.redisHost = redisHost; + this.redisPort = redisPort; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory(){ + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); //key를 string으로 시리얼라이즈 + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); //value를 json으로 시리얼라이즈 + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //Hash의 key를 String으로 시리얼라이즈 + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); //Hash의 value를 json으로 시리얼라이즈 + return redisTemplate; + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/config/S3Config.java b/src/main/java/com/hackathonteam1/refreshrator/config/S3Config.java new file mode 100644 index 0000000..ed23f80 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/config/S3Config.java @@ -0,0 +1,33 @@ +package com.hackathonteam1.refreshrator.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client(){ + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3Client.builder() + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/controller/AuthController.java b/src/main/java/com/hackathonteam1/refreshrator/controller/AuthController.java new file mode 100644 index 0000000..17733a5 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/controller/AuthController.java @@ -0,0 +1,149 @@ +package com.hackathonteam1.refreshrator.controller; + +import com.hackathonteam1.refreshrator.annotation.AuthenticatedUser; +import com.hackathonteam1.refreshrator.authentication.AuthenticationExtractor; +import com.hackathonteam1.refreshrator.authentication.JwtEncoder; +import com.hackathonteam1.refreshrator.dto.ResponseDto; +import com.hackathonteam1.refreshrator.dto.request.auth.LoginDto; +import com.hackathonteam1.refreshrator.dto.request.auth.SigninDto; +import com.hackathonteam1.refreshrator.dto.response.auth.TokenResponseDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeListDto; +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.service.AuthService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.Duration; + +@RestController +@AllArgsConstructor +@RequestMapping("/auth") +public class AuthController { + + private final AuthService authService; + + //회원가입 + @PostMapping("/signin") + public ResponseEntity> signup(@RequestBody @Valid SigninDto signinDto) { + + authService.signin(signinDto); + + return new ResponseEntity<>(ResponseDto.res(HttpStatus.CREATED, "회원 가입 완료"), HttpStatus.CREATED); + } + + //회원 탈퇴 + @DeleteMapping("/leave") + public ResponseEntity> leave(@AuthenticatedUser User user,HttpServletResponse response) { + authService.leave(user); + + ResponseCookie cookie = ResponseCookie.from("AccessToken", null) + .maxAge(0) + .path("/") + .httpOnly(true) + .sameSite("None").secure(true) + .build(); + response.addHeader("set-cookie", cookie.toString()); + + ResponseCookie cookie_refresh = ResponseCookie.from("RefreshToken",null) + .maxAge(0) + .path("/auth/refresh") + .httpOnly(true) + .sameSite("None") + .secure(true) + .build(); + response.addHeader("set-cookie", cookie_refresh.toString()); + + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "회원 탈퇴 완료"), HttpStatus.OK); + } + + //로그인 + @PostMapping("/login") + public ResponseEntity> login(@RequestBody @Valid LoginDto loginDto, HttpServletResponse response) { + + TokenResponseDto tokenResponseDto = authService.login(loginDto); + String bearerToken = JwtEncoder.encodeJwtToken(tokenResponseDto.getAccessToken()); + + ResponseCookie cookie = ResponseCookie.from(AuthenticationExtractor.TOKEN_COOKIE_NAME, bearerToken) + .maxAge(Duration.ofMillis(1800000)) + .path("/") + .httpOnly(true) + .sameSite("None").secure(true) + .build(); + response.addHeader("set-cookie", cookie.toString()); + + ResponseCookie cookie_refresh = ResponseCookie.from("RefreshToken",tokenResponseDto.getRefreshToken().getTokenId().toString()) + .maxAge(Duration.ofDays(14)) + .path("/auth/refresh") + .httpOnly(true) + .sameSite("None") + .secure(true) + .build(); + + response.addHeader("set-cookie", cookie_refresh.toString()); + + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "로그인 완료"), HttpStatus.OK); + } + + //로그아웃 + @PostMapping("/logout") + public ResponseEntity> logout(@AuthenticatedUser User user, final HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from("AccessToken", null) + .maxAge(0) + .path("/") + .httpOnly(true) + .sameSite("None").secure(true) + .build(); + response.addHeader("set-cookie", cookie.toString()); + + ResponseCookie cookie_refresh = ResponseCookie.from("RefreshToken",null) + .maxAge(0) + .path("/auth/refresh") + .httpOnly(true) + .sameSite("None") + .secure(true) + .build(); + response.addHeader("set-cookie", cookie_refresh.toString()); + + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "로그아웃 완료"), HttpStatus.OK); + } + + // 좋아요 누른 레시피 목록 조회 + @GetMapping("/likes") + public ResponseEntity> showAllRecipeLikes(@AuthenticatedUser User user, + @RequestParam(name = "page", defaultValue = "0")int page, + @RequestParam(name = "size", defaultValue = "10")int size) { + RecipeListDto recipeListDto = authService.showAllRecipeLikes(user, page, size); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "좋아요 누른 레시피 목록 조회 성공", recipeListDto), HttpStatus.OK); + } + + @GetMapping("/refresh") + public ResponseEntity> refresh(HttpServletRequest request, HttpServletResponse response){ + TokenResponseDto tokenResponseDto = authService.refresh(request); + String bearerToken = JwtEncoder.encodeJwtToken(tokenResponseDto.getAccessToken()); + + ResponseCookie cookie_access = ResponseCookie.from(AuthenticationExtractor.TOKEN_COOKIE_NAME, bearerToken) + .maxAge(Duration.ofMillis(1800000)) + .path("/") + .httpOnly(true) + .sameSite("None").secure(true) + .build(); + + ResponseCookie cookie_refresh = ResponseCookie.from("RefreshToken", tokenResponseDto.getRefreshToken().getTokenId().toString()) + .maxAge(Duration.ofDays(14)) + .path("/auth/refresh") + .httpOnly(true) + .sameSite("None") + .secure(true) + .build(); + + response.addHeader("set-cookie", cookie_access.toString()); + response.addHeader("set-cookie", cookie_refresh.toString()); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "토큰 재발급 성공"), HttpStatus.OK); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/controller/FridgeController.java b/src/main/java/com/hackathonteam1/refreshrator/controller/FridgeController.java new file mode 100644 index 0000000..e695208 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/controller/FridgeController.java @@ -0,0 +1,59 @@ +package com.hackathonteam1.refreshrator.controller; + +import com.hackathonteam1.refreshrator.annotation.AuthenticatedUser; +import com.hackathonteam1.refreshrator.dto.ResponseDto; +import com.hackathonteam1.refreshrator.dto.request.fridge.AddFridgeDto; +import com.hackathonteam1.refreshrator.dto.response.fridge.FridgeItemListDto; +import com.hackathonteam1.refreshrator.dto.response.fridgeItem.FridgeItemResponseData; +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.service.FridgeService; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@AllArgsConstructor +@RequestMapping("/fridge") +public class FridgeController { + + private final FridgeService fridgeService; + + //냉장고에 재료 추가 + @PostMapping("/ingredients") + public ResponseEntity> addIngredientInFridge(@RequestBody @Valid AddFridgeDto addFridgeDto, @AuthenticatedUser User user) { + fridgeService.addIngredientInFridge(addFridgeDto, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.CREATED, "냉장고에 재료 등록 성공"), HttpStatus.CREATED); + } + + //냉장고에 재료 정보 수정 + @PatchMapping("/ingredients/{fridge_item_id}") + public ResponseEntity> updateIngredientInFridge(@PathVariable("fridge_item_id") UUID id , @RequestBody @Valid AddFridgeDto addFridgeDto, @AuthenticatedUser User user) { + fridgeService.updateIngredientInFridge(id,addFridgeDto, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "냉장고에 재료 수정 성공"), HttpStatus.OK); + } + + //냉장고에 재료 삭제 + @DeleteMapping("/ingredients/{fridge_item_id}") + public ResponseEntity> deleteIngredientInFridge(@PathVariable("fridge_item_id") UUID id , @AuthenticatedUser User user) { + fridgeService.deleteIngredientInFridge(id, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "냉장고에 재료 삭제 성공"), HttpStatus.OK); + } + + // 냉장고에 있는 모든 재료 조회 + @GetMapping("/ingredients") + public ResponseEntity> getIngredientsInFridge(@AuthenticatedUser User user) { + FridgeItemListDto fridgeItemListDto = fridgeService.getIngredientsInFridge(user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "냉장고에 있는 모든 재료 조회 성공", fridgeItemListDto), HttpStatus.OK); + } + + //냉장고에 있는 재료 단건 조회 메서드 + @GetMapping("/ingredients/{fridge_item_id}") + public ResponseEntity> detailIngredientInFridge(@PathVariable("fridge_item_id") UUID id , @AuthenticatedUser User user) { + FridgeItemResponseData fridgeItemResponseData = fridgeService.detailIngredientInFridge(id, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "냉장고에 재료 조회 성공",fridgeItemResponseData),HttpStatus.OK); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/controller/HealthController.java b/src/main/java/com/hackathonteam1/refreshrator/controller/HealthController.java new file mode 100644 index 0000000..1336686 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/controller/HealthController.java @@ -0,0 +1,12 @@ +package com.hackathonteam1.refreshrator.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthController { + @GetMapping("/health") + public String checkHealth(){ + return "ok"; + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/controller/IngredientController.java b/src/main/java/com/hackathonteam1/refreshrator/controller/IngredientController.java new file mode 100644 index 0000000..7d52a89 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/controller/IngredientController.java @@ -0,0 +1,24 @@ +package com.hackathonteam1.refreshrator.controller; + +import com.hackathonteam1.refreshrator.dto.ResponseDto; +import com.hackathonteam1.refreshrator.dto.response.ingredient.IngredientListDto; +import com.hackathonteam1.refreshrator.service.IngredientService; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@AllArgsConstructor +@RequestMapping("/ingredients") +public class IngredientController { + + private final IngredientService ingredientService; + + // DB에 있는 재료 검색 + @GetMapping() + public ResponseEntity> searchIngredientByName(@RequestParam String name) { + IngredientListDto ingredientListDto = ingredientService.searchIngredientByName(name); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "재료 검색 성공", ingredientListDto), HttpStatus.OK); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/controller/RecipeController.java b/src/main/java/com/hackathonteam1/refreshrator/controller/RecipeController.java new file mode 100644 index 0000000..b25d734 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/controller/RecipeController.java @@ -0,0 +1,117 @@ +package com.hackathonteam1.refreshrator.controller; + +import com.hackathonteam1.refreshrator.annotation.AuthenticatedUser; +import com.hackathonteam1.refreshrator.dto.ResponseDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.DeleteIngredientRecipesDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.RegisterIngredientRecipesDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.ModifyRecipeDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.RegisterRecipeDto; +import com.hackathonteam1.refreshrator.dto.response.file.ImageDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.DetailRecipeDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeListDto; +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.service.RecipeService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/recipes") +public class RecipeController { + private final RecipeService recipeService; + + @GetMapping + public ResponseEntity> getList(@RequestParam(name = "keyword",defaultValue = "")String keyword, @RequestParam(name = "type", defaultValue = "newest")String type, + @RequestParam(name = "page", defaultValue = "0")int page, @RequestParam(name = "size", defaultValue = "10")int size){ + RecipeListDto recipeListDto = recipeService.getList(keyword, type, page, size); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK,"레시피 목록 조회 성공", recipeListDto),HttpStatus.OK); + } + + @PostMapping + public ResponseEntity> register( + @RequestBody @Valid RegisterRecipeDto registerRecipeDto, @AuthenticatedUser User user){ + recipeService.register(registerRecipeDto, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.CREATED,"레시피 등록 성공"),HttpStatus.CREATED); + } + + @GetMapping("/{recipe_id}") + public ResponseEntity> getDetail(@PathVariable("recipe_id") UUID recipeId){ + DetailRecipeDto detailRecipeDto = recipeService.getDetail(recipeId); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "레시피 상세 조회 성공", detailRecipeDto),HttpStatus.OK); + } + + @PatchMapping("/{recipe_id}") + public ResponseEntity> modify( + @RequestBody @Valid ModifyRecipeDto modifyRecipeDto, @AuthenticatedUser User user, @PathVariable("recipe_id") UUID recipeId){ + recipeService.modifyContent(modifyRecipeDto, user, recipeId); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK,"레시피 수정 성공"),HttpStatus.OK); + } + + @DeleteMapping("/{recipe_id}") + public ResponseEntity> delete(@PathVariable("recipe_id") UUID recipeId, @AuthenticatedUser User user){ + recipeService.delete(recipeId, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK,"레시피 삭제 성공"),HttpStatus.OK); + } + + @PostMapping("/{recipe_id}/ingredients") + public ResponseEntity> registerIngredientRecipe(@PathVariable("recipe_id") UUID recipeId, + @RequestBody @Valid RegisterIngredientRecipesDto registerIngredientRecipesDto, @AuthenticatedUser User user){ + recipeService.registerIngredientRecipe(user, recipeId, registerIngredientRecipesDto); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.CREATED, "레시피 재료 등록 성공"),HttpStatus.CREATED); + } + + @DeleteMapping("/{recipe_id}/ingredients") + public ResponseEntity> deleteIngredientRecipe(@PathVariable("recipe_id") UUID recipeId, + @RequestBody @Valid DeleteIngredientRecipesDto deleteIngredientRecipesDto, @AuthenticatedUser User user){ + recipeService.deleteIngredientRecipe(user, recipeId, deleteIngredientRecipesDto); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "레시피 재료 삭제 성공"),HttpStatus.OK); + } + + @GetMapping("/recommendations") + public ResponseEntity> getRecommendations( + @RequestParam(name = "page", defaultValue = "0")int page, + @RequestParam(name = "size", defaultValue = "10")int size, + @RequestParam(name = "match", defaultValue = "2147483647")int match, + @RequestParam(name = "type", defaultValue = "newest")String type, + @AuthenticatedUser User user){ + RecipeListDto recipeListDto = recipeService.getRecommendation(page, size, match, type, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK,"추천 레시피 목록 조회 성공", recipeListDto),HttpStatus.OK); + } + + // 레시피에 좋아요 추가 + @PostMapping("/{recipe_id}/likes") + public ResponseEntity> addLikeToRecipe(@PathVariable("recipe_id") UUID recipeId, @AuthenticatedUser User user){ + recipeService.addLikeToRecipe(user, recipeId); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.CREATED, "레시피에 좋아요 추가 성공"),HttpStatus.CREATED); + } + + // 레시피에 좋아요 삭제 + @DeleteMapping("{recipe_id}/likes") + public ResponseEntity> deleteLikeFromRecipe(@PathVariable("recipe_id") UUID recipeId, @AuthenticatedUser User user){ + recipeService.deleteLikeFromRecipe(user, recipeId); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "레시피에 좋아요 삭제 성공"),HttpStatus.OK); + } + + @PostMapping(value = "/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) + public ResponseEntity> registerFile( + @RequestPart MultipartFile file){ + ImageDto imageDto = recipeService.registerImage(file); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "이미지 등록 성공", imageDto),HttpStatus.OK); + } + + @DeleteMapping(value = "/images/{image_Id}") + public ResponseEntity> deleteFile( + @PathVariable UUID image_Id,@AuthenticatedUser User user){ + recipeService.deleteImage(image_Id, user); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "이미지 삭제 성공"),HttpStatus.OK); + + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/controller/UserController.java b/src/main/java/com/hackathonteam1/refreshrator/controller/UserController.java new file mode 100644 index 0000000..03d3fb2 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/controller/UserController.java @@ -0,0 +1,33 @@ +package com.hackathonteam1.refreshrator.controller; + +import com.hackathonteam1.refreshrator.annotation.AuthenticatedUser; +import com.hackathonteam1.refreshrator.dto.ResponseDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeListDto; +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.service.RecipeService; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@RequestMapping("/users") +public class UserController { + + private final RecipeService recipeService; + + @GetMapping("/me/recipes") + public ResponseEntity> getMyRecipe(@AuthenticatedUser User user, + @RequestParam(name = "type", defaultValue = "newest") String type, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "10") int size){ + RecipeListDto recipeListDto = recipeService.findMyRecipes(user, type, page, size); + return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "자신이 작성한 레시피 목록 조회 성공", recipeListDto),HttpStatus.OK); + } + + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/ErrorResponseDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/ErrorResponseDto.java new file mode 100644 index 0000000..ffde7cc --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/ErrorResponseDto.java @@ -0,0 +1,25 @@ +package com.hackathonteam1.refreshrator.dto; + +import com.hackathonteam1.refreshrator.exception.CustomException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ErrorResponseDto { + private final String errorCode; + private final String message; + private final String detail; + + public static ErrorResponseDto res(final CustomException customException){ + String errorCode = customException.getErrorCode().getCode(); + String message = customException.getErrorCode().getMessage(); + String detail = customException.getDetail(); + return new ErrorResponseDto(errorCode,message,detail); + } + + public static ErrorResponseDto res(final String errorCode, final Exception e){ + return new ErrorResponseDto(errorCode, e.getMessage(), null); + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/ResponseDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/ResponseDto.java new file mode 100644 index 0000000..771f43e --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/ResponseDto.java @@ -0,0 +1,22 @@ +package com.hackathonteam1.refreshrator.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; + +@AllArgsConstructor +@Getter +public class ResponseDto { + private final String statusCode; + private final String message; + private final T data; + + public static ResponseDto res(final HttpStatusCode statusCode, final String message){ + return new ResponseDto<>(String.valueOf(statusCode.value()), message, null); + } + + public static ResponseDto res(final HttpStatusCode statusCode, final String message, final T data){ + return new ResponseDto<>(String.valueOf(statusCode.value()), message, data); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/auth/LoginDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/auth/LoginDto.java new file mode 100644 index 0000000..6fb0728 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/auth/LoginDto.java @@ -0,0 +1,24 @@ +package com.hackathonteam1.refreshrator.dto.request.auth; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoginDto { + + //아이디(이메일) + @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{1,100}$", message = "이메일이 형식에 맞지 않습니다.") + @NotBlank(message = "사용하실 아이디(이메일)을 입력해주세요.") + @Size(min = 1,max=100,message = "아이디는 최소 한글자 이상 최대 100글자입니다.") + private String email; + + //비밀번호 + @NotBlank(message = "영문과 숫자,특수기호를 조합하여 8~14글자 미만으로 입력하여 주세요.") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()])[A-Za-z\\d!@#$%^&*()]{8,14}$", message = "영문,숫자,특수기호를 조합하여 8~14글자 미만으로 입력하여 주세요.") + @Size(min = 8, max = 14, message = " 비밀번로는 최소8글자 최대 14글자 입니다.") + private String password; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/auth/SigninDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/auth/SigninDto.java new file mode 100644 index 0000000..fe9ec7a --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/auth/SigninDto.java @@ -0,0 +1,29 @@ +package com.hackathonteam1.refreshrator.dto.request.auth; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SigninDto { + //이름 + @NotBlank(message = "본인의 이름을 입력해주세요.") + @Size(min = 1, max = 20, message = "이름은 최소 한글자 최대 20글자입니다.") + private String name; + + //아이디(이메일) + @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{1,100}$", message = "이메일이 형식에 맞지 않습니다.") + @NotBlank(message = "사용하실 아이디(이메일)을 입력해주세요.") + @Size(min = 1,max=100,message = "아이디는 최소 한글자 이상 최대 100글자입니다.") + private String email; + + //비밀번호 + @NotBlank(message = "영문과 숫자,특수기호를 조합하여 8~14글자 미만으로 입력하여 주세요.") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()])[A-Za-z\\d!@#$%^&*()]{8,14}$", message = "영문,숫자,특수기호를 조합하여 8~14글자 미만으로 입력하여 주세요.") + @Size(min = 8, max = 14, message = " 비밀번로는 최소8글자 최대 14글자 입니다.") + private String password; + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/fridge/AddFridgeDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/fridge/AddFridgeDto.java new file mode 100644 index 0000000..6a0370e --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/fridge/AddFridgeDto.java @@ -0,0 +1,33 @@ +package com.hackathonteam1.refreshrator.dto.request.fridge; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.UUID; + +@Getter +@AllArgsConstructor +public class AddFridgeDto { + + //재료 + @NotNull + private UUID ingredientId; + + //유통기한 설정 + @NotNull + private LocalDate expiredDate; + + //수량 설정 + @NotNull + private Integer quantity; + + //보관 방법 + @NotNull + private String storage; + + //메모 + private String memo; + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/DeleteIngredientRecipesDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/DeleteIngredientRecipesDto.java new file mode 100644 index 0000000..16baf0f --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/DeleteIngredientRecipesDto.java @@ -0,0 +1,26 @@ +package com.hackathonteam1.refreshrator.dto.request.recipe; + +import com.hackathonteam1.refreshrator.exception.BadRequestException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Getter +public class DeleteIngredientRecipesDto { + + @NotNull + private List ingredientRecipeIds; + + public List nonDupIngredientIds(){ + List nonDupIngredientIds = this.getIngredientRecipeIds().stream().distinct().collect(Collectors.toList()); + if(this.getIngredientRecipeIds().size() != nonDupIngredientIds.size()){ + throw new BadRequestException(ErrorCode.DUPLICATED_RECIPE_INGREDIENT); + } + return nonDupIngredientIds; + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/ModifyRecipeDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/ModifyRecipeDto.java new file mode 100644 index 0000000..a88b5d2 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/ModifyRecipeDto.java @@ -0,0 +1,22 @@ +package com.hackathonteam1.refreshrator.dto.request.recipe; + +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ModifyRecipeDto { + + @Size(min = 1, max = 15) + private String name; + + @Size(max = 5000) + private String cookingStep; + + private UUID imageId; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/RegisterIngredientRecipesDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/RegisterIngredientRecipesDto.java new file mode 100644 index 0000000..274fca7 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/RegisterIngredientRecipesDto.java @@ -0,0 +1,24 @@ +package com.hackathonteam1.refreshrator.dto.request.recipe; + +import com.hackathonteam1.refreshrator.exception.BadRequestException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Getter +public class RegisterIngredientRecipesDto { + @NotNull + private List ingredientIds; + + public List nonDupIngredientIds(){ + List nonDupIngredientIds = this.getIngredientIds().stream().distinct().collect(Collectors.toList()); + if(this.ingredientIds.size() != nonDupIngredientIds.size()){ + throw new BadRequestException(ErrorCode.DUPLICATED_RECIPE_INGREDIENT); + } + return nonDupIngredientIds; + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/RegisterRecipeDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/RegisterRecipeDto.java new file mode 100644 index 0000000..d428f05 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/request/recipe/RegisterRecipeDto.java @@ -0,0 +1,33 @@ +package com.hackathonteam1.refreshrator.dto.request.recipe; + +import com.hackathonteam1.refreshrator.entity.Ingredient; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.UUID; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class RegisterRecipeDto { + + @Size(min = 1, max = 15) + @NotBlank + private String name; + + @NotNull + private List ingredientIds; + + @Size(max = 5000) + @NotBlank + private String cookingStep; + + private UUID imageId; + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/PaginationDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/PaginationDto.java new file mode 100644 index 0000000..87f0df1 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/PaginationDto.java @@ -0,0 +1,16 @@ +package com.hackathonteam1.refreshrator.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.domain.Page; + +@Getter +@AllArgsConstructor +public class PaginationDto { + private int currentPage; + private int totalPage; + + public static PaginationDto paginationDto(Page page){ + return new PaginationDto(page.getNumber(), page.getTotalPages()); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/auth/TokenResponseDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/auth/TokenResponseDto.java new file mode 100644 index 0000000..172dd2e --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/auth/TokenResponseDto.java @@ -0,0 +1,14 @@ +package com.hackathonteam1.refreshrator.dto.response.auth; + +import com.hackathonteam1.refreshrator.entity.RefreshToken; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class TokenResponseDto { + private String AccessToken; + private RefreshToken refreshToken; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/file/ImageDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/file/ImageDto.java new file mode 100644 index 0000000..51c0435 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/file/ImageDto.java @@ -0,0 +1,24 @@ +package com.hackathonteam1.refreshrator.dto.response.file; + +import com.hackathonteam1.refreshrator.entity.Image; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.UUID; + +@Getter +@Builder +@AllArgsConstructor +public class ImageDto { + + private UUID id; + private String url; + + public static ImageDto mapping(Image image){ + if(image==null){ + return null; + } + return new ImageDto(image.getId(), image.getUrl()); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridge/FridgeItemDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridge/FridgeItemDto.java new file mode 100644 index 0000000..1311655 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridge/FridgeItemDto.java @@ -0,0 +1,16 @@ +package com.hackathonteam1.refreshrator.dto.response.fridge; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.UUID; + +@Builder +@Getter +public class FridgeItemDto { + private UUID id; + private String ingredientName; + private UUID ingredientId; + private LocalDate expirationDate; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridge/FridgeItemListDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridge/FridgeItemListDto.java new file mode 100644 index 0000000..409b151 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridge/FridgeItemListDto.java @@ -0,0 +1,17 @@ +package com.hackathonteam1.refreshrator.dto.response.fridge; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class FridgeItemListDto { + private List coldStorage; // 냉장 + private List frozen; // 냉동 + private List ambient; // 실온 + private List ExpirationDate; // 유통기한 만료 +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridgeItem/FridgeItemResponseData.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridgeItem/FridgeItemResponseData.java new file mode 100644 index 0000000..50ace0a --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/fridgeItem/FridgeItemResponseData.java @@ -0,0 +1,29 @@ +package com.hackathonteam1.refreshrator.dto.response.fridgeItem; + +import com.hackathonteam1.refreshrator.entity.FridgeItem; +import com.hackathonteam1.refreshrator.entity.Ingredient; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Builder +@Getter +public class FridgeItemResponseData { + + private String ingredientName; + private LocalDate expiredDate; + private Integer quantity; + private FridgeItem.Storage storage; + private String memo; + + public static FridgeItemResponseData fromFridgeItem(FridgeItem fridgeItem) { + return FridgeItemResponseData.builder() + .ingredientName(fridgeItem.getIngredient().getName()) + .expiredDate(fridgeItem.getExpiredDate()) + .quantity(fridgeItem.getQuantity()) + .storage(fridgeItem.getStorage()) + .memo(fridgeItem.getMemo()) + .build(); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/ingredient/IngredientDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/ingredient/IngredientDto.java new file mode 100644 index 0000000..f4c7a43 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/ingredient/IngredientDto.java @@ -0,0 +1,23 @@ +package com.hackathonteam1.refreshrator.dto.response.ingredient; + +import com.hackathonteam1.refreshrator.entity.Ingredient; +import lombok.Builder; +import lombok.Getter; + +import java.util.UUID; + +@Builder +@Getter +public class IngredientDto { + + private UUID id; + private String name; + + public static IngredientDto changeToDto(Ingredient ingredient){ + return IngredientDto.builder() + .id(ingredient.getId()) + .name(ingredient.getName()) + .build(); + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/ingredient/IngredientListDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/ingredient/IngredientListDto.java new file mode 100644 index 0000000..6528848 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/ingredient/IngredientListDto.java @@ -0,0 +1,14 @@ +package com.hackathonteam1.refreshrator.dto.response.ingredient; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class IngredientListDto { + private List ingredients; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/DetailRecipeDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/DetailRecipeDto.java new file mode 100644 index 0000000..c9119c9 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/DetailRecipeDto.java @@ -0,0 +1,36 @@ +package com.hackathonteam1.refreshrator.dto.response.recipe; + +import com.hackathonteam1.refreshrator.dto.response.file.ImageDto; +import com.hackathonteam1.refreshrator.entity.Image; +import com.hackathonteam1.refreshrator.entity.IngredientRecipe; +import com.hackathonteam1.refreshrator.entity.Recipe; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Getter +@Builder +@AllArgsConstructor +public class DetailRecipeDto { + private UUID id; + private String name; + private List ingredientRecipes; + private String cookingStep; + private int likeCount; + private ImageDto image; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static DetailRecipeDto mapping(Recipe recipe, List ingredientRecipes){ + List ingredientDtos = ingredientRecipes.stream().map( + i->IngredientRecipeResponseDto.changeToDto(i)).collect(Collectors.toList()); + return new DetailRecipeDto(recipe.getId(), recipe.getName(), ingredientDtos, + recipe.getCookingStep(), recipe.getLikeCount(), ImageDto.mapping(recipe.getImage()), + recipe.getCreatedAt(), recipe.getUpdatedAt()); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/IngredientRecipeResponseDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/IngredientRecipeResponseDto.java new file mode 100644 index 0000000..0597a1f --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/IngredientRecipeResponseDto.java @@ -0,0 +1,22 @@ +package com.hackathonteam1.refreshrator.dto.response.recipe; + +import com.hackathonteam1.refreshrator.dto.response.ingredient.IngredientDto; +import com.hackathonteam1.refreshrator.entity.Ingredient; +import com.hackathonteam1.refreshrator.entity.IngredientRecipe; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.UUID; + +@Getter +@AllArgsConstructor +public class IngredientRecipeResponseDto { + + private UUID id; + private IngredientDto ingredient; + + public static IngredientRecipeResponseDto changeToDto(IngredientRecipe ingredientRecipe){ + return new IngredientRecipeResponseDto(ingredientRecipe.getId(), IngredientDto.changeToDto(ingredientRecipe.getIngredient())); + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/RecipeDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/RecipeDto.java new file mode 100644 index 0000000..754f39e --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/RecipeDto.java @@ -0,0 +1,25 @@ +package com.hackathonteam1.refreshrator.dto.response.recipe; + +import com.hackathonteam1.refreshrator.dto.response.file.ImageDto; +import com.hackathonteam1.refreshrator.entity.Recipe; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@AllArgsConstructor +public class RecipeDto { + + private UUID recipeId; + private String name; + private int likeCount; + private ImageDto image; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + public static RecipeDto mapping(Recipe recipe){ + return new RecipeDto(recipe.getId(), recipe.getName(), recipe.getLikeCount(), + ImageDto.mapping(recipe.getImage()), recipe.getCreatedAt(), recipe.getUpdatedAt()); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/RecipeListDto.java b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/RecipeListDto.java new file mode 100644 index 0000000..34b920f --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/dto/response/recipe/RecipeListDto.java @@ -0,0 +1,28 @@ +package com.hackathonteam1.refreshrator.dto.response.recipe; + +import com.hackathonteam1.refreshrator.dto.response.PaginationDto; +import com.hackathonteam1.refreshrator.entity.Recipe; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@AllArgsConstructor +@Builder +public class RecipeListDto { + private List recipeList; + private PaginationDto pagination; + + public static RecipeListDto mapping(Page page){ + return RecipeListDto.builder() + .recipeList(page.stream() + .map((i-> RecipeDto.mapping(i))) + .collect(Collectors.toList())) + .pagination(PaginationDto.paginationDto(page)) + .build(); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/BaseEntity.java b/src/main/java/com/hackathonteam1/refreshrator/entity/BaseEntity.java new file mode 100644 index 0000000..398f7e1 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/BaseEntity.java @@ -0,0 +1,27 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + + +import java.time.LocalDateTime; +import java.util.UUID; + +@MappedSuperclass// 상속을 받는 Entity 클래스에게 매핑 정보만 제공 +@Getter +@EntityListeners(AuditingEntityListener.class)// AuditingEntityListener는 엔티티의 생성 및 갱신 시간을 자동으로 설정하는 역할을 한다. +public class BaseEntity { + //PK 생성 + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "uuid2") + @Column(updatable = false, unique = true, nullable = false) + private UUID id; + @CreatedDate + private LocalDateTime createdAt; + @LastModifiedDate + private LocalDateTime updatedAt; +} + diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/Fridge.java b/src/main/java/com/hackathonteam1/refreshrator/entity/Fridge.java new file mode 100644 index 0000000..181d0c9 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/Fridge.java @@ -0,0 +1,24 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.repository.cdi.Eager; + +import java.util.List; + +//fridge와 user는 단방향. +@Entity(name = "fridge") +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Fridge extends BaseEntity{ + + @OneToMany(mappedBy = "fridge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List fridgeItem; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id",nullable = false) + private User user; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/FridgeItem.java b/src/main/java/com/hackathonteam1/refreshrator/entity/FridgeItem.java new file mode 100644 index 0000000..c0f226c --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/FridgeItem.java @@ -0,0 +1,43 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; + +@Entity(name = "fridge_item") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class FridgeItem extends BaseEntity{ + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "fridge_id", nullable = false) + private Fridge fridge; + + @ManyToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "ingredient_id", nullable = false) + private Ingredient ingredient; + + @Column(nullable = false) + private LocalDate expiredDate; + + @Column + private int quantity; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Storage storage; + + @Column + private String memo; + + public enum Storage{ + STORE_AT_ROOM_TEMPERATURE, REFRIGERATED, FROZEN; + } + public boolean isExpired(){ + return LocalDate.now().isAfter(this.expiredDate); + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/Image.java b/src/main/java/com/hackathonteam1/refreshrator/entity/Image.java new file mode 100644 index 0000000..35158f2 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/Image.java @@ -0,0 +1,23 @@ +package com.hackathonteam1.refreshrator.entity; + + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.validator.internal.util.stereotypes.Lazy; + +import java.util.UUID; + +@Entity(name = "image") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Image extends BaseEntity{ + + @Column(nullable = false) + private String url; //aws s3의 url + + @OneToOne(mappedBy = "image", cascade = CascadeType.PERSIST) + private Recipe recipe; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/Ingredient.java b/src/main/java/com/hackathonteam1/refreshrator/entity/Ingredient.java new file mode 100644 index 0000000..3265e01 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/Ingredient.java @@ -0,0 +1,26 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; +import java.util.List; + +@Entity +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Ingredient extends BaseEntity{ + + @Column(nullable = false) + private String name; + + @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "ingredient", fetch = FetchType.LAZY) + private List fridgeItems; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "ingredient", fetch = FetchType.LAZY) + private List ingredientRecipes; + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/IngredientRecipe.java b/src/main/java/com/hackathonteam1/refreshrator/entity/IngredientRecipe.java new file mode 100644 index 0000000..afb5324 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/IngredientRecipe.java @@ -0,0 +1,22 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity(name = "ingredient_recipe") +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class IngredientRecipe extends BaseEntity{ + + @ManyToOne(optional = false, fetch = FetchType.EAGER) + @JoinColumn(name = "ingredient_id", nullable = false) + private Ingredient ingredient; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/Recipe.java b/src/main/java/com/hackathonteam1/refreshrator/entity/Recipe.java new file mode 100644 index 0000000..30c7f5c --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/Recipe.java @@ -0,0 +1,64 @@ +package com.hackathonteam1.refreshrator.entity; + +import com.hackathonteam1.refreshrator.exception.ConflictException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Formula; + +import java.util.List; + +@Entity(name = "recipe") +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Recipe extends BaseEntity{ + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + @Lob + private String cookingStep; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private User user; + + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List recipeLikes; + + @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List ingredientRecipes; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "image_id") + @Nullable + private Image image; + + @Formula("(select count(rl.id) from recipe_like rl where rl.recipe_id = id)") + private int likeCount; + + public void updateName(String name){ + this.name = name; + } + public void updateCookingStep(String cookingStep){ + this.cookingStep = cookingStep; + } + public void updateImage(Image image){ + if(this.image != null){ + throw new ConflictException(ErrorCode.RECIPE_IMAGE_CONFLICT); + } + this.image = image; + } + + public Boolean isContainingImage(){ + return this.getImage()!=null; + } + + public void deleteImage(){ + this.image = null; + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/RecipeLike.java b/src/main/java/com/hackathonteam1/refreshrator/entity/RecipeLike.java new file mode 100644 index 0000000..227cdb1 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/RecipeLike.java @@ -0,0 +1,27 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity(name = "recipe_like") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RecipeLike extends BaseEntity{ + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/RefreshToken.java b/src/main/java/com/hackathonteam1/refreshrator/entity/RefreshToken.java new file mode 100644 index 0000000..6976a40 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/RefreshToken.java @@ -0,0 +1,22 @@ +package com.hackathonteam1.refreshrator.entity; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.index.Indexed; + +import java.util.UUID; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RefreshToken { + @Id + private UUID tokenId; + @Indexed + private UUID userId; +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/entity/User.java b/src/main/java/com/hackathonteam1/refreshrator/entity/User.java new file mode 100644 index 0000000..5aa28de --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/entity/User.java @@ -0,0 +1,35 @@ +package com.hackathonteam1.refreshrator.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Entity(name = "user") +public class User extends BaseEntity{ + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String password; + + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) + private List recipes; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List recipeLikes; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private Fridge fridge; +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/BadRequestException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/BadRequestException.java new file mode 100644 index 0000000..9c1572c --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/BadRequestException.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class BadRequestException extends CustomException{ + public BadRequestException(ErrorCode errorCode) { + super(errorCode); + } + + public BadRequestException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/ConflictException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/ConflictException.java new file mode 100644 index 0000000..5d09a58 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/ConflictException.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class ConflictException extends CustomException{ + public ConflictException(ErrorCode errorCode) { + super(errorCode); + } + + public ConflictException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/CustomException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/CustomException.java new file mode 100644 index 0000000..b0d1dac --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/CustomException.java @@ -0,0 +1,23 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException{ + + private final ErrorCode errorCode; + private final String detail; + + public CustomException(ErrorCode errorCode){ + this.errorCode = errorCode; + this.detail = null; + } + + public CustomException(ErrorCode errorCode, String detail){ + this.errorCode = errorCode; + this.detail = detail; + } + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/DtoValidationException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/DtoValidationException.java new file mode 100644 index 0000000..f4f5bd3 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/DtoValidationException.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class DtoValidationException extends CustomException{ + public DtoValidationException(ErrorCode errorCode) { + super(errorCode); + } + + public DtoValidationException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/FileStorageException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/FileStorageException.java new file mode 100644 index 0000000..0097d7f --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/FileStorageException.java @@ -0,0 +1,15 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class FileStorageException extends CustomException{ + + + public FileStorageException(ErrorCode errorCode) { + super(errorCode); + } + + public FileStorageException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/ForbiddenException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/ForbiddenException.java new file mode 100644 index 0000000..33f3637 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/ForbiddenException.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class ForbiddenException extends CustomException{ + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } + + public ForbiddenException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/NotFoundException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/NotFoundException.java new file mode 100644 index 0000000..6591d97 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/NotFoundException.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class NotFoundException extends CustomException{ + public NotFoundException(ErrorCode errorCode) { + super(errorCode); + } + + public NotFoundException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/RedisException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/RedisException.java new file mode 100644 index 0000000..a162510 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/RedisException.java @@ -0,0 +1,9 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class RedisException extends CustomException{ + public RedisException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/UnauthorizedException.java b/src/main/java/com/hackathonteam1/refreshrator/exception/UnauthorizedException.java new file mode 100644 index 0000000..92b507a --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/UnauthorizedException.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.exception; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; + +public class UnauthorizedException extends CustomException{ + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } + + public UnauthorizedException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/advice/ExceptionController.java b/src/main/java/com/hackathonteam1/refreshrator/exception/advice/ExceptionController.java new file mode 100644 index 0000000..af2f5a0 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/advice/ExceptionController.java @@ -0,0 +1,76 @@ +package com.hackathonteam1.refreshrator.exception.advice; + +import com.hackathonteam1.refreshrator.dto.ErrorResponseDto; +import com.hackathonteam1.refreshrator.exception.CustomException; +import com.hackathonteam1.refreshrator.exception.DtoValidationException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.ValidationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.support.MetaDataAccessException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ExceptionController { + + @ExceptionHandler(CustomException.class) + public ResponseEntity handleCustomException(CustomException customException){ + writeLog(customException); + HttpStatus httpStatus = this.resolveHttpStatus(customException); + return new ResponseEntity<>(ErrorResponseDto.res(customException), httpStatus); + } + + @ExceptionHandler({ValidationException.class, MethodArgumentNotValidException.class}) + public ResponseEntity handleCustomException(MethodArgumentNotValidException methodArgumentNotValidException){ + FieldError fieldError = methodArgumentNotValidException.getBindingResult().getFieldError(); + if(fieldError == null){ + return new ResponseEntity<>(ErrorResponseDto.res(String.valueOf(HttpStatus.BAD_REQUEST.value()), + methodArgumentNotValidException), HttpStatus.BAD_REQUEST); + } + ErrorCode validationErrorCode = ErrorCode.resolveValidationErrorCode(fieldError.getCode()); + String detail = fieldError.getDefaultMessage(); + DtoValidationException dtoValidationException = new DtoValidationException(validationErrorCode, detail); + this.writeLog(dtoValidationException); + return new ResponseEntity<>(ErrorResponseDto.res(dtoValidationException),HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException entityNotFoundException){ + writeLog(entityNotFoundException); + return new ResponseEntity<>(ErrorResponseDto.res(String.valueOf(HttpStatus.NOT_FOUND.value()),entityNotFoundException), HttpStatus.NOT_FOUND); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ResponseEntity handleExceptioon(Exception exception){ + this.writeLog(exception); + return new ResponseEntity<>( + ErrorResponseDto.res(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), exception), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + private void writeLog(CustomException customException){ + String exceptionName = customException.getClass().getSimpleName(); + ErrorCode errorCode = customException.getErrorCode(); + String detail = customException.getDetail(); + log.error("[{}]{}:{}", exceptionName,errorCode.getMessage(), detail); + } + + private void writeLog(Exception exception){ + String exceptionName = exception.getClass().getSimpleName(); + String message = exception.getMessage(); + log.error("[{}]:{}", exceptionName, message); + } + + private HttpStatus resolveHttpStatus(CustomException customException){ + return HttpStatus.resolve(Integer.parseInt(customException.getErrorCode().getCode().substring(0,3))); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/exception/errorcode/ErrorCode.java b/src/main/java/com/hackathonteam1/refreshrator/exception/errorcode/ErrorCode.java new file mode 100644 index 0000000..f70300d --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/exception/errorcode/ErrorCode.java @@ -0,0 +1,67 @@ +package com.hackathonteam1.refreshrator.exception.errorcode; + +import com.hackathonteam1.refreshrator.entity.Recipe; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + + //BadRequestException + SIZE("4000", "길이가 유효하지 않습니다."), + PATTERN("4001","형식에 맞지 않습니다."), + NOT_BLANK("4002", "필수값이 공백입니다."), + LENGTH("4003", "길이가 유효하지 않습니다."), + EMAIL("4004", "이메일 형식이 유효하지 않습니다."), + NOT_NULL("4005", "필수값이 공백입니다."), + DUPLICATED_RECIPE_INGREDIENT("4006","중복되는 레시피 재료 관련 요청은 불가합니다."), + FILE_TYPE_ERROR("4007", "유효하지 않은 파일 형식입니다."), + IMAGE_NOT_IN_RECIPE("4008", "이미지가 존재하지 않는 레시피입니다."), + + //AuthorizedException + COOKIE_NOT_FOUND("4010", "쿠키를 찾을 수 없습니다."), + INVALID_TOKEN("4011", "유효하지 않은 토큰입니다."), + INVALID_PASSWORD("4012","검증되지 않은 비밀번호입니다."), + + //ForbiddenException + RECIPE_FORBIDDEN("4030","해당 레시피에 대한 권한이 없습니다."), + FRIDGE_ITEM_FORBIDDEN("4031","해당 재료정보에 대한 권한이 없습니다."), + + //NotFoundException + USERID_NOT_FOUND("4040","존재하지 않는 사용자 입니다"), + INGREDIENT_NOT_FOUND("4041", "재료를 찾을 수 없습니다."), + RECIPE_NOT_FOUND("4042", "레시피를 찾을 수 없습니다."), + INGREDIENT_RECIPE_NOT_FOUND("4043", "레시피의 재료를 찾을 수 없습니다."), + FRIDGE_NOT_FOUND("4044","냉장고를 찾을 수 없습니다"), + FRIDGE_ITEM_NOT_FOUND("4045","냉장고에 등록된 재료 정보를 찾을 수 없습니다."), + PAGE_NOT_FOUND("4046", "페이지를 찾을 수 없습니다"), + IMAGE_NOT_FOUND("4047","이미지를 찾을 수 없습니다"), + RECIPE_LIKE_NOT_FOUND("4048", "좋아요를 누른 레시피가 아닙니다."), + + + //ConflictException + DUPLICATED_EMAIL("4090", "이미 사용 중인 이메일입니다."), + RECIPE_INGREDIENT_CONFLICT("4091", "이미 해당 레시피에 존재하는 재료입니다."), + USER_ALREADY_ADD_LIKE("4092", "해당 레시피는 이미 좋아요를 누른 레시피입니다."), + RECIPE_IMAGE_CONFLICT("4093", "레시피에 이미지가 이미 존재합니다."), + + //InternetException + FILE_STORAGE_ERROR("5000", "파일을 업로드할 수 없습니다."), + REDIS_ERROR("5001", "Redis에서 오류가 발생했습니다."); + + private final String code; + private final String message; + + public static ErrorCode resolveValidationErrorCode(String code){ + return switch (code){ + case "Size" -> SIZE; + case "Pattern" -> PATTERN; + case "NotBlank" -> NOT_BLANK; + case "Length" -> LENGTH; + case "Email" -> EMAIL; + case "NotNull" -> NOT_NULL; + default -> throw new IllegalArgumentException("Unexpected value: "+ code); + }; + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/FridgeItemRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/FridgeItemRepository.java new file mode 100644 index 0000000..58e2ff5 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/FridgeItemRepository.java @@ -0,0 +1,9 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.FridgeItem; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface FridgeItemRepository extends JpaRepository { +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/FridgeRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/FridgeRepository.java new file mode 100644 index 0000000..309ce5b --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/FridgeRepository.java @@ -0,0 +1,12 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.Fridge; +import com.hackathonteam1.refreshrator.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface FridgeRepository extends JpaRepository { + Optional findByUser(User user); +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/ImageRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/ImageRepository.java new file mode 100644 index 0000000..6fbef8e --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/ImageRepository.java @@ -0,0 +1,9 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.Image; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface ImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/IngredientRecipeRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/IngredientRecipeRepository.java new file mode 100644 index 0000000..5a0feb5 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/IngredientRecipeRepository.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.IngredientRecipe; +import com.hackathonteam1.refreshrator.entity.Recipe; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface IngredientRecipeRepository extends JpaRepository { + Optional> findAllByRecipe(Recipe recipe); +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/IngredientRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/IngredientRepository.java new file mode 100644 index 0000000..4e9f04d --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/IngredientRepository.java @@ -0,0 +1,9 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.Ingredient; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface IngredientRepository extends JpaRepository { +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/RecipeLikeRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/RecipeLikeRepository.java new file mode 100644 index 0000000..8071738 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/RecipeLikeRepository.java @@ -0,0 +1,17 @@ +package com.hackathonteam1.refreshrator.repository; + + +import com.hackathonteam1.refreshrator.entity.RecipeLike; +import com.hackathonteam1.refreshrator.entity.User; +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.UUID; + +@Repository +public interface RecipeLikeRepository extends JpaRepository { + Page findAllByUser(User user, Pageable pageable); +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/RecipeRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/RecipeRepository.java new file mode 100644 index 0000000..5076b83 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/RecipeRepository.java @@ -0,0 +1,22 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.Ingredient; +import com.hackathonteam1.refreshrator.entity.Recipe; +import com.hackathonteam1.refreshrator.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Set; +import java.util.UUID; + +public interface RecipeRepository extends JpaRepository { + Page findAllByNameContaining(String keyword, Pageable pageable); + + @Query("SELECT r FROM recipe r JOIN r.ingredientRecipes ir JOIN ir.ingredient i WHERE i IN :ingredients GROUP BY r.id HAVING COUNT(i) >= :match OR COUNT(i) = SIZE(r.ingredientRecipes)") + Page findAllByIngredientRecipesContain(@Param("ingredients") Set ingredients, @Param("match") int match, Pageable pageable); + Page findAll(Pageable pageable); + Page findAllByUser(User user, Pageable pageable); +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/repository/UserRepository.java b/src/main/java/com/hackathonteam1/refreshrator/repository/UserRepository.java new file mode 100644 index 0000000..5c70d35 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.hackathonteam1.refreshrator.repository; + +import com.hackathonteam1.refreshrator.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository extends JpaRepository { + User findByEmail(String email); +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/service/AuthService.java b/src/main/java/com/hackathonteam1/refreshrator/service/AuthService.java new file mode 100644 index 0000000..de2e9a3 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/service/AuthService.java @@ -0,0 +1,183 @@ +package com.hackathonteam1.refreshrator.service; + +import com.hackathonteam1.refreshrator.authentication.JwtEncoder; +import com.hackathonteam1.refreshrator.authentication.JwtTokenProvider; +import com.hackathonteam1.refreshrator.authentication.PasswordHashEncryption; +import com.hackathonteam1.refreshrator.dto.request.auth.LoginDto; +import com.hackathonteam1.refreshrator.dto.request.auth.SigninDto; +import com.hackathonteam1.refreshrator.dto.response.auth.TokenResponseDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeListDto; +import com.hackathonteam1.refreshrator.entity.*; +import com.hackathonteam1.refreshrator.exception.ConflictException; +import com.hackathonteam1.refreshrator.exception.ForbiddenException; +import com.hackathonteam1.refreshrator.exception.NotFoundException; +import com.hackathonteam1.refreshrator.exception.UnauthorizedException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import com.hackathonteam1.refreshrator.repository.FridgeRepository; +import com.hackathonteam1.refreshrator.repository.RecipeLikeRepository; +import com.hackathonteam1.refreshrator.repository.UserRepository; +import com.hackathonteam1.refreshrator.util.RedisUtil; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +@Slf4j +public class AuthService { + private final UserRepository userRepository; + private final FridgeRepository fridgeRepository; + private final PasswordHashEncryption passwordHashEncryption; + private final JwtTokenProvider jwtTokenProvider; + private final RecipeLikeRepository recipeLikeRepository; + + private final RedisUtil redisUtilForRefreshToken; + private final RedisUtil redisUtilForUserId; + + private final static int TIMEOUT = 14; + private final static TimeUnit TIME_UNIT = TimeUnit.DAYS; + + + //회원가입 + public void signin(SigninDto signinDto){ + + //아이디(이메일) 중복 방지 + User user=userRepository.findByEmail(signinDto.getEmail()); + if(user!=null){ + throw new ConflictException(ErrorCode.DUPLICATED_EMAIL); + } + + //비밀번호 암호화 + String plainPassword = signinDto.getPassword(); + String hashedPassword = passwordHashEncryption.encrypt(plainPassword); + + //유저 생성과 등록 + User signinUser=User.builder() + .name(signinDto.getName()) + .email(signinDto.getEmail()) + .password(hashedPassword) + .build(); + userRepository.save(signinUser); + + //냉장고 생성 + Fridge fridge= Fridge.builder() + .user(signinUser) + .build(); + fridgeRepository.save(fridge); + } + + //회원탈퇴 + public void leave(User user){ + //탈퇴 + userRepository.delete(user); + } + + //로그인 + public TokenResponseDto login(LoginDto loginDto){ + + //아이디(이메일)검사 + User user=userRepository.findByEmail(loginDto.getEmail()); + + if(user==null){ + throw new NotFoundException(ErrorCode.USERID_NOT_FOUND); + } + + //비밀번호가 입력한 아이디에 일치하는지 검사 + if(!passwordHashEncryption.matches(loginDto.getPassword(), user.getPassword())){ + throw new ForbiddenException(ErrorCode.INVALID_PASSWORD); + } + + //토큰 생성 + String payload = String.valueOf(user.getId()); + String accessToken = jwtTokenProvider.createToken(payload); + + //기존에 refreshToken이 있었는지 확인 후 삭제 + Optional refreshTokenId = redisUtilForUserId.findById(user.getId().toString()); + if(refreshTokenId.isPresent()){ + redisUtilForRefreshToken.delete(refreshTokenId.get()); + redisUtilForUserId.delete(user.getId().toString()); + } + + UUID newRefreshTokenId = UUID.randomUUID(); + RefreshToken refreshToken = RefreshToken.builder() + .tokenId(newRefreshTokenId) + .userId(user.getId()) + .build(); + + redisUtilForRefreshToken.save(newRefreshTokenId.toString(), refreshToken, TIMEOUT, TIME_UNIT); + redisUtilForUserId.save(user.getId().toString(), newRefreshTokenId.toString(),TIMEOUT,TIME_UNIT); + + return new TokenResponseDto(accessToken, refreshToken); + } + + // 좋아요 누른 레시피 목록 조회 + public RecipeListDto showAllRecipeLikes(User user, int page, int size) { + List recipeLists = new ArrayList<>(); + + Sort sort = Sort.by(Sort.Order.desc("createdAt")); + + Pageable pageable = PageRequest.of(page, size); + + Page recipeLikes = this.recipeLikeRepository.findAllByUser(user, pageable); + List recipes = recipeLikes.stream().map(like -> like.getRecipe()).collect(Collectors.toList()); + Page recipePage = new PageImpl<>(recipes); + + checkValidPage(recipePage, page); + + RecipeListDto recipeListDto = RecipeListDto.mapping(recipePage); + return recipeListDto; + } + + @Transactional + public TokenResponseDto refresh(HttpServletRequest request){ + + Cookie[] cookies = request.getCookies(); + + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("RefreshToken")) { + RefreshToken refreshToken = findRefreshTokenByRefreshTokenId(UUID.fromString(cookie.getValue())); + UUID userId = refreshToken.getUserId(); + String accessToken = jwtTokenProvider.createToken(userId.toString()); + + //refreshToken Rotation을 위해 매번 재발급. + redisUtilForRefreshToken.delete(refreshToken.getTokenId().toString()); + redisUtilForUserId.delete(userId.toString()); + + UUID newRefreshTokenId = UUID.randomUUID(); + + RefreshToken newRefreshToken = RefreshToken.builder() + .tokenId(newRefreshTokenId) + .userId(userId) + .build(); + + redisUtilForRefreshToken.save(newRefreshTokenId.toString(), newRefreshToken, TIMEOUT, TIME_UNIT); + redisUtilForUserId.save(userId.toString(), newRefreshTokenId.toString(),TIMEOUT,TIME_UNIT); + + return new TokenResponseDto(accessToken, newRefreshToken); + } + } + } + throw new UnauthorizedException(ErrorCode.COOKIE_NOT_FOUND, "RefreshToken이 존재하지 않습니다."); + } + + private RefreshToken findRefreshTokenByRefreshTokenId(UUID tokenId){ + return redisUtilForRefreshToken.findById(tokenId.toString()).orElseThrow( () -> + new UnauthorizedException(ErrorCode.INVALID_TOKEN, "유효하지 않은 RefreshToken입니다.")); + } + + private void checkValidPage(Page pages, int page){ + if(pages.getTotalPages() <= page && page != 0){ + throw new NotFoundException(ErrorCode.PAGE_NOT_FOUND); + } + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/service/FridgeService.java b/src/main/java/com/hackathonteam1/refreshrator/service/FridgeService.java new file mode 100644 index 0000000..4492264 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/service/FridgeService.java @@ -0,0 +1,171 @@ +package com.hackathonteam1.refreshrator.service; + +import com.hackathonteam1.refreshrator.dto.request.fridge.AddFridgeDto; +import com.hackathonteam1.refreshrator.dto.response.fridge.FridgeItemDto; +import com.hackathonteam1.refreshrator.dto.response.fridge.FridgeItemListDto; +import com.hackathonteam1.refreshrator.dto.response.fridgeItem.FridgeItemResponseData; +import com.hackathonteam1.refreshrator.entity.Fridge; +import com.hackathonteam1.refreshrator.entity.FridgeItem; +import com.hackathonteam1.refreshrator.entity.Ingredient; +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.exception.ForbiddenException; +import com.hackathonteam1.refreshrator.exception.NotFoundException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import com.hackathonteam1.refreshrator.repository.FridgeItemRepository; +import com.hackathonteam1.refreshrator.repository.FridgeRepository; +import com.hackathonteam1.refreshrator.repository.IngredientRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@AllArgsConstructor +public class FridgeService { + private IngredientRepository ingredientRepository; + private FridgeItemRepository fridgeItemRepository; + private FridgeRepository fridgeRepository; + + //냉장고에 재료 추가 + public void addIngredientInFridge(AddFridgeDto addFridgeDto, User user){ + + //재료 찾기 + Ingredient ingredient=findIngredient(addFridgeDto); + + //냉장고 찾기 + Fridge fridge=findFridge(user); + + //재료 정보 입력 + FridgeItem fridgeItem=FridgeItem.builder() + .fridge(fridge) + .ingredient(ingredient) + .expiredDate(addFridgeDto.getExpiredDate()) + .quantity(addFridgeDto.getQuantity()) + .storage(defindStorage(addFridgeDto.getStorage())) + .memo(addFridgeDto.getMemo()) + .build(); + + //재료를 냉장고에 저장 + fridgeItemRepository.save(fridgeItem); + } + + //냉장고에 재료 정보 수정 메서드 + public void updateIngredientInFridge(UUID fridgeItemId, AddFridgeDto addFridgeDto, User user){ + + //수정할 재료 찾기 + FridgeItem fridgeItem=findFridgeItem(fridgeItemId); + + //유저가 등록한 재료인지 검사 + checkAuth(fridgeItem.getFridge().getUser(),user); + + //수정하기 + fridgeItem.setExpiredDate(addFridgeDto.getExpiredDate()); + fridgeItem.setQuantity(addFridgeDto.getQuantity()); + fridgeItem.setStorage(defindStorage(addFridgeDto.getStorage())); + fridgeItem.setMemo(addFridgeDto.getMemo()); + + //저장하기 + fridgeItemRepository.save(fridgeItem); + } + + //냉장고에 재료 삭제 메서드 + public void deleteIngredientInFridge(UUID fridgeItemId, User user){ + //삭제할 재료 찾기 + FridgeItem fridgeItem=findFridgeItem(fridgeItemId); + + //권한 확인 + checkAuth(fridgeItem.getFridge().getUser(),user); + + //삭제하기 + fridgeItemRepository.delete(fridgeItem); + } + + // 냉장고에 모든 재료 조회 + public FridgeItemListDto getIngredientsInFridge(User user) { + // 유저의 냉장고 찾기 + Fridge fridge = findFridge(user); + + // 권힌 확인 + checkAuth(fridge.getUser(),user); + + List coldStorageList = new ArrayList<>(); // 냉장 재료 리스트 + List frozenStorageList = new ArrayList<>(); // 냉동 재료 리스트 + List ambientStorageList = new ArrayList<>(); // 실온 재료 리스트 + List expirationDateList = new ArrayList<>(); // 유통 기한 만료 재료 리스트 + + for(FridgeItem fridgeItem : fridge.getFridgeItem()){ + FridgeItemDto fridgeItemDto = FridgeItemDto.builder() + .id(fridgeItem.getId()) + .ingredientId(fridgeItem.getIngredient().getId()) + .ingredientName(fridgeItem.getIngredient().getName()) + .expirationDate(fridgeItem.getExpiredDate()) + .build(); + + if(LocalDate.now().isAfter(fridgeItem.getExpiredDate())){ + // 유통 기한 만료 재료 + expirationDateList.add(fridgeItemDto); + } else if(fridgeItem.getStorage().equals(FridgeItem.Storage.STORE_AT_ROOM_TEMPERATURE)){ + // 실온 보관인 경우 + ambientStorageList.add(fridgeItemDto); + } else if(fridgeItem.getStorage().equals(FridgeItem.Storage.REFRIGERATED)){ + // 냉장 보관인 경우 + coldStorageList.add(fridgeItemDto); + } else if(fridgeItem.getStorage().equals(FridgeItem.Storage.FROZEN)){ + // 냉동 보관인 경우 + frozenStorageList.add(fridgeItemDto); + } + + } + return new FridgeItemListDto(coldStorageList, frozenStorageList, ambientStorageList, expirationDateList); + } + + //냉장고에 있는 재료 단건 조회 메서드 + public FridgeItemResponseData detailIngredientInFridge(UUID fridgeItemId, User user){ + //조회할 재료 찾기 + FridgeItem fridgeItem=findFridgeItem(fridgeItemId); + + //권한 확인 + checkAuth(fridgeItem.getFridge().getUser(),user); + + //조회 하기 + return FridgeItemResponseData.fromFridgeItem(fridgeItem); + } + + + //저장방법 결정 메서드 + private FridgeItem.Storage defindStorage(String storage){ + return switch (storage){ + case "상온"-> FridgeItem.Storage.STORE_AT_ROOM_TEMPERATURE; + case "냉동"-> FridgeItem.Storage.FROZEN; + default -> FridgeItem.Storage.REFRIGERATED; + }; + } + + //데이터베이스에서 재료id로 재료를 찾는 메서드 + private Ingredient findIngredient(AddFridgeDto addFridgeDto){ + return ingredientRepository.findById(addFridgeDto.getIngredientId()) + .orElseThrow(()-> new NotFoundException(ErrorCode.INGREDIENT_NOT_FOUND)); + } + + //데이터베이스에서 유저의 냉장고를 찾는 메서드 + private Fridge findFridge(User user){ + return fridgeRepository.findByUser(user) + .orElseThrow(()-> new NotFoundException(ErrorCode.FRIDGE_NOT_FOUND)); + } + + //냉장고에서 요청한 재료를 찾는 메서드 + private FridgeItem findFridgeItem(UUID id){ + return fridgeItemRepository.findById(id) + .orElseThrow(()-> new NotFoundException(ErrorCode.FRIDGE_ITEM_NOT_FOUND)); + } + + //권한 확인 + private void checkAuth(User writer, User user){ + if(!writer.getId().equals(user.getId())){ + throw new ForbiddenException(ErrorCode.FRIDGE_ITEM_FORBIDDEN); + } + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/service/IngredientService.java b/src/main/java/com/hackathonteam1/refreshrator/service/IngredientService.java new file mode 100644 index 0000000..89fde95 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/service/IngredientService.java @@ -0,0 +1,38 @@ +package com.hackathonteam1.refreshrator.service; + +import com.hackathonteam1.refreshrator.dto.response.ingredient.IngredientDto; +import com.hackathonteam1.refreshrator.dto.response.ingredient.IngredientListDto; +import com.hackathonteam1.refreshrator.entity.Ingredient; +import com.hackathonteam1.refreshrator.exception.NotFoundException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import com.hackathonteam1.refreshrator.repository.IngredientRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@AllArgsConstructor +public class IngredientService { + private final IngredientRepository ingredientRepository; + + public IngredientListDto searchIngredientByName(String name) { + List ingredients = ingredientRepository.findAll(); + List ingredientDtoList = new ArrayList<>(); + + for(Ingredient ingredient : ingredients) { + if(ingredient.getName().contains(name)) { // 해당 검색어를 포함하는 모든 재료를 찾는다. + IngredientDto ingredientDto = IngredientDto.builder() + .id(ingredient.getId()) + .name(ingredient.getName()) + .build(); + ingredientDtoList.add(ingredientDto); + } + } + if(ingredientDtoList.isEmpty()) { + throw new NotFoundException(ErrorCode.INGREDIENT_NOT_FOUND); + } + return new IngredientListDto(ingredientDtoList); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/service/RecipeService.java b/src/main/java/com/hackathonteam1/refreshrator/service/RecipeService.java new file mode 100644 index 0000000..2be73b3 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/service/RecipeService.java @@ -0,0 +1,59 @@ +package com.hackathonteam1.refreshrator.service; + +import com.hackathonteam1.refreshrator.dto.request.recipe.DeleteIngredientRecipesDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.RegisterIngredientRecipesDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.ModifyRecipeDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.RegisterRecipeDto; +import com.hackathonteam1.refreshrator.dto.response.file.ImageDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.DetailRecipeDto; + +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeListDto; +import com.hackathonteam1.refreshrator.entity.Recipe; +import com.hackathonteam1.refreshrator.entity.User; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +public interface RecipeService { + + //레시피 목록 조회 + public RecipeListDto getList(String keyword, String type, int page, int size); + + //레시피 등록 + public void register(RegisterRecipeDto registerRecipeDto, User user); + + //레시피 상세조회 + public DetailRecipeDto getDetail(UUID recipeId); + + //레시피 내용 수정 + public void modifyContent(ModifyRecipeDto modifyRecipeDto, User user, UUID recipeId); + + //레시피 삭제 + public void delete(UUID recipeId, User user); + + //레시피 재료 추가 + public void registerIngredientRecipe(User user, UUID recipeId, RegisterIngredientRecipesDto registerIngredientRecipesDto); + + //레시피 재료 삭제 + public void deleteIngredientRecipe(User user, UUID recipeId, DeleteIngredientRecipesDto deleteIngredientRecipesDto); + + + //추천 레시피 목록 조회 + public RecipeListDto getRecommendation(int page, int size, int match, String type, User user); + + // 레시피에 좋아요 추가 + public void addLikeToRecipe(User user, UUID recipeId); + + // 레시피에 좋아요 삭제 + public void deleteLikeFromRecipe(User user, UUID recipeId); + + //파일(이미지) 등록 + public ImageDto registerImage(MultipartFile file); + + //파일(이미지) 삭제 + public void deleteImage(UUID imageId, User user); + +// 자신이 작성한 레시피 조회 + public RecipeListDto findMyRecipes(User user, String type, int page, int size); + +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/service/RecipeServiceImpl.java b/src/main/java/com/hackathonteam1/refreshrator/service/RecipeServiceImpl.java new file mode 100644 index 0000000..9299a9d --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/service/RecipeServiceImpl.java @@ -0,0 +1,361 @@ +package com.hackathonteam1.refreshrator.service; + +import com.hackathonteam1.refreshrator.dto.request.recipe.DeleteIngredientRecipesDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.RegisterIngredientRecipesDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.ModifyRecipeDto; +import com.hackathonteam1.refreshrator.dto.request.recipe.RegisterRecipeDto; +import com.hackathonteam1.refreshrator.dto.response.file.ImageDto; +import com.hackathonteam1.refreshrator.dto.response.recipe.DetailRecipeDto; +import com.hackathonteam1.refreshrator.entity.*; + +import com.hackathonteam1.refreshrator.dto.response.recipe.RecipeListDto; + +import com.hackathonteam1.refreshrator.entity.Ingredient; +import com.hackathonteam1.refreshrator.entity.IngredientRecipe; +import com.hackathonteam1.refreshrator.entity.Recipe; +import com.hackathonteam1.refreshrator.entity.User; +import com.hackathonteam1.refreshrator.entity.Fridge; +import com.hackathonteam1.refreshrator.entity.Image; + +import com.hackathonteam1.refreshrator.exception.*; + +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import com.hackathonteam1.refreshrator.repository.*; +import com.hackathonteam1.refreshrator.repository.FridgeRepository; +import com.hackathonteam1.refreshrator.repository.ImageRepository; +import com.hackathonteam1.refreshrator.repository.IngredientRecipeRepository; +import com.hackathonteam1.refreshrator.repository.IngredientRepository; +import com.hackathonteam1.refreshrator.repository.RecipeRepository; +import com.hackathonteam1.refreshrator.util.S3Uploader; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.MediaType; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional +public class RecipeServiceImpl implements RecipeService{ + private final RecipeRepository recipeRepository; + private final IngredientRepository ingredientRepository; + private final IngredientRecipeRepository ingredientRecipeRepository; + private final FridgeRepository fridgeRepository; + private final S3Uploader s3Uploader; + private final ImageRepository imageRepository; + private final RecipeLikeRepository recipeLikeRepository; + + @Override + public RecipeListDto getList(String keyword, String type, int page, int size) { + + Sort sort; + if (type.equals("newest")){ + sort = Sort.by(Sort.Order.desc("createdAt")); + }else{ + sort = Sort.by(Sort.Order.desc("likeCount")); + } + + Pageable pageable = PageRequest.of(page, size, sort); + + if(keyword.equals("")){ + Page recipePage = recipeRepository.findAll(pageable); + checkValidPage(recipePage, page); + return RecipeListDto.mapping(recipePage); + } + + Page recipePage = recipeRepository.findAllByNameContaining(keyword, pageable); + checkValidPage(recipePage, page); + return RecipeListDto.mapping(recipePage); + } + + @Override + @Transactional + public void register(RegisterRecipeDto registerRecipeDto, User user) { + Image image = null; + + if(registerRecipeDto.getImageId()!=null){ + image = findImageByImageId(registerRecipeDto.getImageId()); + } + + Recipe recipe = Recipe.builder() + .name(registerRecipeDto.getName()) + .cookingStep(registerRecipeDto.getCookingStep()) + .user(user) + .image(image) + .build(); + + recipeRepository.save(recipe); + + registerRecipeDto.getIngredientIds().stream().forEach(i -> registerRecipeIngredient(findIngredientByIngredientId(i),recipe)); + } + + //상세조회 + @Override + public DetailRecipeDto getDetail(UUID recipeId) { + Recipe recipe = findRecipeByRecipeId(recipeId); + List ingredientRecipes = findAllIngredientRecipeByRecipe(recipe); + + DetailRecipeDto detailRecipeDto = DetailRecipeDto.mapping(recipe, ingredientRecipes); + return detailRecipeDto; + } + + //레시피명, 조리법 수정 + @Override + public void modifyContent(ModifyRecipeDto modifyRecipeDto, User user, UUID recipeId) { + Recipe recipe = findRecipeByRecipeId(recipeId); + checkAuth(recipe.getUser(), user); + + Image image = null; + + if(modifyRecipeDto.getImageId()!=null){ + recipe.updateImage(findImageByImageId(modifyRecipeDto.getImageId())); + } + if(modifyRecipeDto.getName()!=null){ + recipe.updateName(modifyRecipeDto.getName()); + } + if(modifyRecipeDto.getCookingStep()!=null) { + recipe.updateCookingStep(modifyRecipeDto.getCookingStep()); + } + + recipeRepository.save(recipe); + } + + //레시피 삭제 + @Override + public void delete(UUID recipeId, User user) { + Recipe recipe = findRecipeByRecipeId(recipeId); + checkAuth(recipe.getUser(), user); + recipeRepository.delete(recipe); + } + + //레시피 재료 등록 + @Override + @Transactional + public void registerIngredientRecipe(User user, UUID recipeId, RegisterIngredientRecipesDto registerIngredientRecipesDto) { + Recipe recipe = findRecipeByRecipeId(recipeId); + checkAuth(recipe.getUser(), user); + + //기존에 레시피에 존재하던 재료 리스트 + List ingredients = findAllIngredientByIngredientRecipes(findAllIngredientRecipeByRecipe(recipe)); + + HashSet existingIngredients = new HashSet<>(ingredients); + + List newIngredients = registerIngredientRecipesDto.nonDupIngredientIds().stream().map(i-> + findIngredientByIngredientId(i)).collect(Collectors.toList()); + + //기존에 레시피에 존재하던 재료인지 확인 후 추가 + newIngredients.stream().forEach(newIngredient->{ + if(existingIngredients.contains(newIngredient)){ + throw new ConflictException(ErrorCode.RECIPE_INGREDIENT_CONFLICT); + } + registerRecipeIngredient(newIngredient, recipe); + }); + } + + @Override + public void deleteIngredientRecipe(User user, UUID recipeId, DeleteIngredientRecipesDto deleteIngredientRecipesDto) { + Recipe recipe = findRecipeByRecipeId(recipeId); + checkAuth(recipe.getUser(),user); + + //기존 레시피의 재료들 + List existingIngredientRecipes = findAllIngredientRecipeByRecipe(recipe); + + Set existingIngredientRecipeIds = existingIngredientRecipes.stream().map(i->i.getId()).collect(Collectors.toSet()); + + deleteIngredientRecipesDto.nonDupIngredientIds().forEach(i -> { + if(!existingIngredientRecipeIds.contains(i)){ + throw new NotFoundException(ErrorCode.INGREDIENT_RECIPE_NOT_FOUND); + } + ingredientRecipeRepository.deleteById(i); + }); + + } + + @Override + public RecipeListDto getRecommendation(int page, int size, int match, String type, User user) { + Set userFridgeItems = findFridgeByUser(user).getFridgeItem().stream() + .filter(fridgeItem -> !fridgeItem.isExpired()) + .collect(Collectors.toSet()); + + Set usersIngredients = userFridgeItems.stream().map(i -> i.getIngredient()).collect(Collectors.toSet()); + + Sort sort; + if (type.equals("popularity")){ + sort = Sort.by(Sort.Order.desc("likeCount")); + }else{ + sort = Sort.by(Sort.Order.desc("createdAt")); + } + + Pageable pageable = PageRequest.of(page,size,sort); + + Page resultPages = recipeRepository.findAllByIngredientRecipesContain(usersIngredients, match, pageable); + checkValidPage(resultPages, page); + RecipeListDto recipeListDto = RecipeListDto.mapping(resultPages); + return recipeListDto; + } + + @Override + public ImageDto registerImage(MultipartFile file) { + + if(!file.getContentType().equals(MediaType.IMAGE_GIF_VALUE) && + !file.getContentType().equals(MediaType.IMAGE_PNG_VALUE) && + !file.getContentType().equals(MediaType.IMAGE_JPEG_VALUE) ){ + throw new FileStorageException(ErrorCode.FILE_TYPE_ERROR); + } + + String url; + try { + url = s3Uploader.upload(file); + } catch (IOException e) { + throw new FileStorageException(ErrorCode.FILE_STORAGE_ERROR, e.getMessage()); + } + + Image image = Image.builder() + .url(url) + .build(); + + imageRepository.save(image); + ImageDto imageDto = ImageDto.mapping(image); + return imageDto; + } + + @Override + public void deleteImage(UUID imageId, User user) { + Image image = findImageByImageId(imageId); + Recipe recipe = image.getRecipe(); + + if(!recipe.isContainingImage()){ + throw new BadRequestException(ErrorCode.IMAGE_NOT_IN_RECIPE); + } + + checkAuth(recipe.getUser(), user); + recipe.deleteImage(); + s3Uploader.removeS3File(image.getUrl().split("/")[3]); + imageRepository.delete(image); + } + + @Override + public RecipeListDto findMyRecipes(User user, String type, int page, int size) { + + Sort sort; + if (type.equals("popularity")){ + sort = Sort.by(Sort.Order.desc("likeCount")); + }else{ + sort = Sort.by(Sort.Order.desc("createdAt")); + } + + Pageable pageable = PageRequest.of(page,size, sort); + Page recipePage = recipeRepository.findAllByUser(user, pageable); + checkValidPage(recipePage, page); + + RecipeListDto recipeListDto = RecipeListDto.mapping(recipePage); + + return recipeListDto; + } + + private Fridge findFridgeByUser(User user){ + return fridgeRepository.findByUser(user).orElseThrow(()-> new NotFoundException(ErrorCode.FRIDGE_NOT_FOUND)); + } + + private Image findImageByImageId(UUID imageId){ + return imageRepository.findById(imageId).orElseThrow(()->new NotFoundException(ErrorCode.IMAGE_NOT_FOUND)); + } + + private Ingredient findIngredientByIngredientId(UUID ingredientId){ + return ingredientRepository.findById(ingredientId).orElseThrow(()-> new NotFoundException(ErrorCode.INGREDIENT_NOT_FOUND)); + } + + //RecipeIngredient를 등록하는 메서드 + private void registerRecipeIngredient(Ingredient ingredient, Recipe recipe){ + IngredientRecipe ingredientRecipe = IngredientRecipe.builder() + .recipe(recipe) + .ingredient(ingredient) + .build(); + ingredientRecipeRepository.save(ingredientRecipe); + } + + // 레시피에 좋아요 추가 + @Override + public void addLikeToRecipe(User user, UUID recipeId){ + Recipe recipe = findRecipeByRecipeId(recipeId); + + // 유저가 이미 좋아요를 누른 레시피인지 확인 + this.isUserAlreadyAddLike(user, recipe); + + // 좋아요를 누른 레시피가 아니라면 좋아요 추가 + RecipeLike recipeLike = new RecipeLike(user, recipe); + recipe.getRecipeLikes().add(recipeLike); + this.recipeRepository.save(recipe); + } + + // 레시피에 좋아요 삭제 + @Override + public void deleteLikeFromRecipe(User user, UUID recipeId){ + Recipe recipe = findRecipeByRecipeId(recipeId); + // 해당 레시피에서 내가 누른 좋아요 반환 + RecipeLike recipeLike = this.findMyRecipeLike(user, recipe); + recipe.getUser().getRecipeLikes().remove(recipeLike); + this.recipeLikeRepository.delete(recipeLike); + } + + // 유저가 이미 좋아요를 누른 레시피인지 확인 + public void isUserAlreadyAddLike(User user, Recipe recipe){ + for (RecipeLike recipeLike : recipe.getRecipeLikes()) { + if(recipeLike.getUser().getId().equals(user.getId())){ + throw new ConflictException(ErrorCode.USER_ALREADY_ADD_LIKE); + } + } + } + + // 해당 레시피에서 내가 누른 좋아요 반환 + public RecipeLike findMyRecipeLike(User user, Recipe recipe){ + for (RecipeLike recipeLike : recipe.getRecipeLikes()) { + if(recipeLike.getUser().getId().equals(user.getId())){ + return recipeLike; + } + } + throw new NotFoundException(ErrorCode.RECIPE_LIKE_NOT_FOUND); + } + + private Recipe findRecipeByRecipeId(UUID recipeId){ + return recipeRepository.findById(recipeId).orElseThrow(()-> new NotFoundException(ErrorCode.RECIPE_NOT_FOUND)); + } + + //IngredientRecipe 리스트로 Ingredient 리스트 생성하는 메서드 + private List findAllIngredientByIngredientRecipes(List ingredientRecipes){ + return ingredientRecipes.stream().map(i->i.getIngredient()).collect(Collectors.toList()); + } + + //Recipe로 해당 Recipe 내에 존재하는 IngredientRecipe 리스트를 반환하는 메서드 + private List findAllIngredientRecipeByRecipe(Recipe recipe){ + return ingredientRecipeRepository.findAllByRecipe(recipe).orElseThrow(()-> new NotFoundException(ErrorCode.INGREDIENT_RECIPE_NOT_FOUND)); + } + + //권한 확인 + private void checkAuth(User writer, User user){ + if(!writer.getId().equals(user.getId())){ + throw new ForbiddenException(ErrorCode.RECIPE_FORBIDDEN); + } + } + + private void checkValidPage(Page pages, int page){ + if(pages.getTotalPages() <= page && page != 0){ + throw new NotFoundException(ErrorCode.PAGE_NOT_FOUND); + } + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/util/RedisUtil.java b/src/main/java/com/hackathonteam1/refreshrator/util/RedisUtil.java new file mode 100644 index 0000000..8ca3e23 --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/util/RedisUtil.java @@ -0,0 +1,40 @@ +package com.hackathonteam1.refreshrator.util; + + +import com.hackathonteam1.refreshrator.exception.RedisException; +import com.hackathonteam1.refreshrator.exception.errorcode.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Service +@AllArgsConstructor +@Slf4j +public class RedisUtil { + private final RedisTemplate redisTemplate; + + public void save(K key, V value, long timeout, TimeUnit timeUnit) { + try { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } catch (Exception e) { + throw new RedisException( ErrorCode.REDIS_ERROR, e.getMessage()); + } + } + + public void delete(K key) { + try { + redisTemplate.delete(key); + }catch (Exception e){ + throw new RedisException( ErrorCode.REDIS_ERROR, e.getMessage()); + } + } + + public Optional findById(K key){ + V result = redisTemplate.opsForValue().get(key); + return Optional.ofNullable(result); + } +} diff --git a/src/main/java/com/hackathonteam1/refreshrator/util/S3Uploader.java b/src/main/java/com/hackathonteam1/refreshrator/util/S3Uploader.java new file mode 100644 index 0000000..d92a99d --- /dev/null +++ b/src/main/java/com/hackathonteam1/refreshrator/util/S3Uploader.java @@ -0,0 +1,41 @@ +package com.hackathonteam1.refreshrator.util; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class S3Uploader { + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + public String bucket; + + + public String upload(MultipartFile multipartFile) throws IOException { + String fileName = multipartFile.getName() + UUID.randomUUID(); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(multipartFile.getSize()); + metadata.setContentType(multipartFile.getContentType()); + + amazonS3Client.putObject(bucket, fileName, multipartFile.getInputStream(), metadata); + return amazonS3Client.getUrl(bucket, fileName).toString(); + } + + + public void removeS3File(String fileName){ + final DeleteObjectRequest deleteObjectRequest = new DeleteObjectRequest(bucket, fileName); + amazonS3Client.deleteObject(deleteObjectRequest); + } + +} diff --git a/src/test/java/com/hackathonteam1/refreshrator/RefreshratorApplicationTests.java b/src/test/java/com/hackathonteam1/refreshrator/RefreshratorApplicationTests.java new file mode 100644 index 0000000..5ed0a67 --- /dev/null +++ b/src/test/java/com/hackathonteam1/refreshrator/RefreshratorApplicationTests.java @@ -0,0 +1,13 @@ +package com.hackathonteam1.refreshrator; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +// +//@SpringBootTest +//class RefreshratorApplicationTests { +// +// @Test +// void contextLoads() { +// } +// +//}