diff --git a/back-end/roughcode/.gitignore b/back-end/roughcode/.gitignore new file mode 100644 index 00000000..1f948445 --- /dev/null +++ b/back-end/roughcode/.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/ + +### yml ### +*.yml \ No newline at end of file diff --git a/back-end/roughcode/build.gradle b/back-end/roughcode/build.gradle new file mode 100644 index 00000000..498308b0 --- /dev/null +++ b/back-end/roughcode/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '2.7.10' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' +} + +group = 'com.cody' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + // swagger + implementation 'org.springdoc:springdoc-openapi-ui:1.6.14' + implementation 'com.google.code.gson:gson:2.9.0' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + implementation('com.auth0:java-jwt:4.2.1') + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + + //redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + implementation 'org.hibernate:hibernate-validator:6.2.5.Final' + testImplementation 'org.glassfish:jakarta.el' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/back-end/roughcode/gradle/wrapper/gradle-wrapper.jar b/back-end/roughcode/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/back-end/roughcode/gradle/wrapper/gradle-wrapper.jar differ diff --git a/back-end/roughcode/gradle/wrapper/gradle-wrapper.properties b/back-end/roughcode/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..774fae87 --- /dev/null +++ b/back-end/roughcode/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/back-end/roughcode/gradlew b/back-end/roughcode/gradlew new file mode 100644 index 00000000..a69d9cb6 --- /dev/null +++ b/back-end/roughcode/gradlew @@ -0,0 +1,240 @@ +#!/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/master/subprojects/plugins/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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"' + +# 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 + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +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/back-end/roughcode/gradlew.bat b/back-end/roughcode/gradlew.bat new file mode 100644 index 00000000..f127cfd4 --- /dev/null +++ b/back-end/roughcode/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% 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/back-end/roughcode/settings.gradle b/back-end/roughcode/settings.gradle new file mode 100644 index 00000000..83595ebf --- /dev/null +++ b/back-end/roughcode/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'roughcode' diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/RoughcodeApplication.java b/back-end/roughcode/src/main/java/com/cody/roughcode/RoughcodeApplication.java new file mode 100644 index 00000000..8b6f5d6c --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/RoughcodeApplication.java @@ -0,0 +1,15 @@ +package com.cody.roughcode; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class RoughcodeApplication { + + public static void main(String[] args) { + SpringApplication.run(RoughcodeApplication.class, args); + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeFavorites.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeFavorites.java new file mode 100644 index 00000000..d9f39cf8 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeFavorites.java @@ -0,0 +1,34 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.code.entity.Codes; +import com.cody.roughcode.user.entity.Users; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "code_favorites") +public class CodeFavorites { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "favorites_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long favoritesId; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "codes_id", nullable = false) + private Codes codes; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id") + private Users users; + + @Builder.Default + @Column(name = "content", nullable = true, columnDefinition = "text") + private String content = ""; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeSelectedTags.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeSelectedTags.java new file mode 100644 index 00000000..a9fc42b6 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeSelectedTags.java @@ -0,0 +1,28 @@ +package com.cody.roughcode.code.entity; + +import com.cody.roughcode.project.entity.ProjectTags; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "code_selected_tags") +public class CodeSelectedTags { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "selected_tags_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long selectedTagsId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tags_id", nullable = false) + private CodeTags tags; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "codes_id", nullable = false) + private Codes codes; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeTags.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeTags.java new file mode 100644 index 00000000..06f23e34 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodeTags.java @@ -0,0 +1,27 @@ +package com.cody.roughcode.code.entity; + +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "code_tags") +public class CodeTags { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tags_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long tagsId; + + @Column(name = "name", length = 255, nullable = false) + private String name; + + @Builder.Default + @Column(name = "cnt", nullable = true) + private int cnt = 0; + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/Codes.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/Codes.java new file mode 100644 index 00000000..72f82d44 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/Codes.java @@ -0,0 +1,51 @@ +package com.cody.roughcode.code.entity; + +import com.cody.roughcode.project.entity.Projects; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.util.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; +import java.util.List; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "codes") +public class Codes extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "codes_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long codesId; + + @Column(name = "num", nullable = false) + private Long num; + + @Column(name = "version", nullable = false) + private int version; + + @Column(name = "title", nullable = false, length = 63) + private String title; + + @Builder.Default + @Column(name = "like_cnt", nullable = true) + private int likeCnt = 0; + + @ManyToOne + @JoinColumn(name = "code_writer_id", nullable = false) + private Users codeWriter; + + @Builder.Default + @Column(name = "review_cnt", nullable = true) + private int reviewCnt = 0; + + @OneToMany(mappedBy = "codes") + private List tags; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "projects_id", nullable = false) + private Projects projects; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodesInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodesInfo.java new file mode 100644 index 00000000..04bc369f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/CodesInfo.java @@ -0,0 +1,36 @@ +package com.cody.roughcode.code.entity; + +import com.cody.roughcode.project.entity.Projects; +import lombok.*; + +import javax.persistence.*; +import java.util.List; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "codes_info") +public class CodesInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long id; + + @Builder.Default + @Column(name = "content", nullable = true, columnDefinition = "text") + private String content = ""; + @Builder.Default + @Column(name = "favorite_cnt", nullable = true) + private int favoriteCnt = 0; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "codes_id", nullable = false) + private Codes codes; + + @OneToMany(mappedBy = "codes") + private List reviews; + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/ReReviews.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/ReReviews.java new file mode 100644 index 00000000..f672a6f0 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/ReReviews.java @@ -0,0 +1,41 @@ +package com.cody.roughcode.code.entity; + +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.util.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "rereviews") +public class ReReviews extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "rereviews_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long reReviewsId; + + @Builder.Default + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id") + private Users users = null; + + @Column(name = "content", nullable = false, columnDefinition = "text") + private String content; + + @Builder.Default + @Column(name = "like_cnt", nullable = true) + private int likeCnt = 0; + + @Builder.Default + @Column(name = "complaint", nullable = true) + private int complaint = 0; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reviews_id", nullable = false) + private Reviews reviews; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/Reviews.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/Reviews.java new file mode 100644 index 00000000..64590049 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/entity/Reviews.java @@ -0,0 +1,61 @@ +package com.cody.roughcode.code.entity; + +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.util.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; +import java.util.List; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "reviews") +public class Reviews extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "reviews_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long reviewsId; + + @Builder.Default + @Column(name = "content", nullable = false, columnDefinition = "text") + private String content = ""; + + @Builder.Default + @Column(name = "like_cnt", nullable = true) + private int likeCnt = 0; + + @Builder.Default + @Column(name = "complaint", nullable = true) + private int complaint = 0; + + @Builder.Default + @Column(name = "line_numbers", nullable = true, columnDefinition = "text") + private String lineNumbers = ""; + + @Column(name = "comment", columnDefinition = "text") + private String comment; + + @Column(name = "review_code", columnDefinition = "longtext") + private String reviewCode; + + @Builder.Default + @Column(name = "selected", nullable = true) + private boolean selected = false; + + + @OneToMany(mappedBy = "reviews") + private List reviewsRereviewss; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "codes_id", nullable = false) + private Codes codes; + + @Builder.Default + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id") + private Users users = null; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/code/repository/CodesRepostiory.java b/back-end/roughcode/src/main/java/com/cody/roughcode/code/repository/CodesRepostiory.java new file mode 100644 index 00000000..b51e754a --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/code/repository/CodesRepostiory.java @@ -0,0 +1,8 @@ +package com.cody.roughcode.code.repository; + +import com.cody.roughcode.code.entity.Codes; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CodesRepostiory extends JpaRepository { + Codes findByCodesId(Long id); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/config/AwsS3Config.java b/back-end/roughcode/src/main/java/com/cody/roughcode/config/AwsS3Config.java new file mode 100644 index 00000000..d294795f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/config/AwsS3Config.java @@ -0,0 +1,30 @@ +package com.cody.roughcode.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsS3Config { + @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 awsCreds = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) + .build(); + } +} \ No newline at end of file diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/config/CorsConfig.java b/back-end/roughcode/src/main/java/com/cody/roughcode/config/CorsConfig.java new file mode 100644 index 00000000..628c39c3 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/config/CorsConfig.java @@ -0,0 +1,29 @@ +package com.cody.roughcode.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.setAllowedOrigins( + Arrays.asList("http://localhost:3000", "http://j8a306.p.ssafy.io", "http://j8a306.p.ssafy.io:3000")); + config.addAllowedOriginPattern("*"); + config.setAllowedHeaders(Arrays.asList("*")); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/config/RedisConfig.java b/back-end/roughcode/src/main/java/com/cody/roughcode/config/RedisConfig.java new file mode 100644 index 00000000..4071f975 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/config/RedisConfig.java @@ -0,0 +1,47 @@ +package com.cody.roughcode.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@RequiredArgsConstructor +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.redis.host}") + private String host; + @Value("${spring.redis.port}") + private int port; + @Value("${spring.redis.password}") + private String password; + + // RedisTemplate를 이용한 방식 + // RedisConnectionFactory 인터페이스를 통해 LettuceConnectionFactory를 생성하여 반환 + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port); + redisStandaloneConfiguration.setPassword(password); + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + // setKeySerializer, setValueSerializer 설정 + // redis-cli을 통해 직접 데이터를 조회시 알아볼 수 없는 형태로 출력되는 것을 방지 + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/config/SecurityConfig.java b/back-end/roughcode/src/main/java/com/cody/roughcode/config/SecurityConfig.java new file mode 100644 index 00000000..eb96243a --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/config/SecurityConfig.java @@ -0,0 +1,79 @@ +package com.cody.roughcode.config; + +import com.cody.roughcode.security.auth.JwtAuthenticationFilter; +import com.cody.roughcode.security.auth.JwtExceptionFilter; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.security.handler.AuthenticationFailureHandler; +import com.cody.roughcode.security.handler.CustomLogoutHandler; +import com.cody.roughcode.security.handler.AuthenticationSuccessHandler; +import com.cody.roughcode.security.oauth2.CookieOAuth2AuthorizationRequestRepository; +import com.cody.roughcode.security.oauth2.CustomOAuth2AuthorizationRequestRepository; +import com.cody.roughcode.security.oauth2.CustomOAuth2UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + private final CorsConfig corsConfig; +// private final CustomOAuth2AuthorizationRequestRepository customOAuth2AuthorizationRequestRepository; + private final CookieOAuth2AuthorizationRequestRepository cookieOAuth2AuthorizationRequestRepository; + private final AuthenticationSuccessHandler authenticationSuccessHandler; + private final AuthenticationFailureHandler authenticationFailureHandler; + + private final CustomLogoutHandler customLogoutHandler; + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/user/token", "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs/**", "/favicon.ico"); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, CustomOAuth2UserService customOAuth2UserService) throws Exception { + http + .httpBasic().disable() // 기본 로그인 화면 비활성화 + .formLogin().disable() // 폼로그인 비활성화 + .csrf().disable() // csrf 보안 비활성화 + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt 사용으로 session 비활성화 + .and() + .logout() + .logoutUrl("/logout") // 로그아웃 처리 URL +// .logoutSuccessUrl("/login") // 로그아웃 성공후 이동할 페이지 + .deleteCookies("accessToken", "refreshToken") // 쿠키 삭제 + .addLogoutHandler(customLogoutHandler)// 로그아웃 구현할 class 넣기 + .and() + .authorizeRequests() + .anyRequest().permitAll()//authenticated() // 인가 검증 + .and() + .oauth2Login() + .authorizationEndpoint(authorize -> { + // 프론트엔드에서 백엔드로 소셜로그인 요청을 보내는 URI + authorize.baseUri("/api/v1/oauth2/authorization"); + // Authorization 과정에서 기본으로 Session을 사용하지만 Cookie로 변경하기 위해 설정함 + authorize.authorizationRequestRepository(cookieOAuth2AuthorizationRequestRepository); + }) + .userInfoEndpoint(userInfo -> { // Provider로부터 획득한 유저정보를 다룰 service class 지정함 + userInfo.userService(customOAuth2UserService); + }) + .successHandler(authenticationSuccessHandler) // OAuth2 로그인 성공시 호출할 handler + .failureHandler(authenticationFailureHandler) // OAuth2 로그인 실패시 호출할 handler + .and() + .addFilter(corsConfig.corsFilter()) // cors 설정. 일단 전부 풀어놓음 + // 모든 request에서 JWT를 검사할 filter를 추가함 + // UsernamePasswordAuthenticationFilter에서 클라이언트가 요청한 리소스의 접근권한이 없을 때 막는 역할을 하기 때문에 + // 이 필터 전에 jwtAuthenticationFilter 실행 + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class); + return http.build(); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/BadRequestException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/BadRequestException.java new file mode 100644 index 00000000..5778046f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/BadRequestException.java @@ -0,0 +1,15 @@ +package com.cody.roughcode.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } + + public BadRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/DeletionFailException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/DeletionFailException.java new file mode 100644 index 00000000..de5e4e8f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/DeletionFailException.java @@ -0,0 +1,8 @@ +package com.cody.roughcode.exception; + +public class DeletionFailException extends RuntimeException { + public DeletionFailException(String message) { + super(message + " 삭제에 실패했습니다"); + } +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NoTokenException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NoTokenException.java new file mode 100644 index 00000000..68a78c25 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NoTokenException.java @@ -0,0 +1,12 @@ +package com.cody.roughcode.exception; + +public class NoTokenException extends RuntimeException { + public NoTokenException(String message) { + super(message); + } + + public NoTokenException() { + super("토큰이 없습니다"); + } +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NotMatchException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NotMatchException.java new file mode 100644 index 00000000..2077829d --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NotMatchException.java @@ -0,0 +1,12 @@ +package com.cody.roughcode.exception; + +public class NotMatchException extends RuntimeException { + public NotMatchException(String message) { + super(message); + } + + public NotMatchException() { + super("접근 권한이 없습니다"); + } +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NotNewestVersionException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NotNewestVersionException.java new file mode 100644 index 00000000..c94d7f03 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/NotNewestVersionException.java @@ -0,0 +1,12 @@ +package com.cody.roughcode.exception; + +import org.springframework.dao.DataAccessException; + +public class NotNewestVersionException extends DataAccessException { + public NotNewestVersionException(String message) { + super(message); + } + public NotNewestVersionException() { + super("최신 버전이 아닙니다"); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/SaveFailedException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/SaveFailedException.java new file mode 100644 index 00000000..cada9974 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/SaveFailedException.java @@ -0,0 +1,9 @@ +package com.cody.roughcode.exception; + +import org.springframework.dao.DataAccessException; + +public class SaveFailedException extends DataAccessException { + public SaveFailedException(String message) { + super(message); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/exception/UpdateFailedException.java b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/UpdateFailedException.java new file mode 100644 index 00000000..103837d6 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/exception/UpdateFailedException.java @@ -0,0 +1,9 @@ +package com.cody.roughcode.exception; + +import org.springframework.dao.DataAccessException; + +public class UpdateFailedException extends DataAccessException { + public UpdateFailedException(String message) { + super(message); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/controller/ProjectsController.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/controller/ProjectsController.java new file mode 100644 index 00000000..fe8a5658 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/controller/ProjectsController.java @@ -0,0 +1,89 @@ +package com.cody.roughcode.project.controller; + +import com.cody.roughcode.project.dto.req.ProjectReq; +import com.cody.roughcode.project.service.ProjectsServiceImpl; +import com.cody.roughcode.security.auth.JwtProperties; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.util.Response; +import io.lettuce.core.ScriptOutputType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 static com.cody.roughcode.security.auth.JwtProperties.TOKEN_HEADER; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/api/v1/project") +@RequiredArgsConstructor +@Slf4j +public class ProjectsController { + private final JwtTokenProvider jwtTokenProvider; + private final ProjectsServiceImpl projectsService; + + @Operation(summary = "프로젝트 수정 API") + @PutMapping("/content") + ResponseEntity updateProject(@CookieValue(name = JwtProperties.ACCESS_TOKEN) String accessToken, + @Parameter(description = "프로젝트 정보 값", required = true) @RequestBody ProjectReq req) { + Long userId = jwtTokenProvider.getId(accessToken); +// Long userId = 1L; + + int res = 0; + try{ + res = projectsService.updateProject(req, userId); + } catch (Exception e){ + log.error(e.getMessage()); + return Response.badRequest(e.getMessage()); + } + + if(res == 0) return Response.notFound("프로젝트 정보 수정 실패"); + return Response.ok("프로젝트 정보 수정 성공"); + } + + @Operation(summary = "프로젝트 썸네일 등록/수정 API") + @PostMapping(value = "/thumbnail", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + ResponseEntity updateProjectThumbnail(@CookieValue(name = JwtProperties.ACCESS_TOKEN) String accessToken, + @Parameter(description = "등록할 project id", required = true) @RequestParam("projectId") Long projectId, + @Parameter(description = "등록할 썸네일", required = true) @RequestPart("thumbnail") MultipartFile thumbnail) { + Long userId = jwtTokenProvider.getId(accessToken); +// Long userId = 1L; + + int res = 0; + try{ + res = projectsService.updateProjectThumbnail(thumbnail, projectId, userId); + } catch (Exception e){ + log.error(e.getMessage()); + return Response.badRequest(e.getMessage()); + } + + if(res == 0) return Response.notFound("프로젝트 썸네일 등록 실패"); + return Response.ok("프로젝트 썸네일 등록 성공"); + } + + @Operation(summary = "프로젝트 정보 등록 API") + @PostMapping("/content") + ResponseEntity insertProject(@CookieValue(name = JwtProperties.ACCESS_TOKEN) String accessToken, + @Parameter(description = "프로젝트 정보 값", required = true) @RequestBody ProjectReq req) { + Long userId = jwtTokenProvider.getId(accessToken); +// Long userId = 2L; + + Long res = 0L; + try{ + res = projectsService.insertProject(req, userId); + } catch (Exception e){ + log.error(e.getMessage()); + return Response.badRequest(e.getMessage()); + } + + if(res <= 0) return Response.notFound("프로젝트 정보 등록 실패"); + return Response.makeResponse(HttpStatus.OK, "프로젝트 정보 등록 성공", 1, res); + } +} \ No newline at end of file diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/dto/req/ProjectReq.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/dto/req/ProjectReq.java new file mode 100644 index 00000000..85a580ef --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/dto/req/ProjectReq.java @@ -0,0 +1,43 @@ +package com.cody.roughcode.project.dto.req; + +import com.cody.roughcode.project.entity.ProjectSelectedTags; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.persistence.Column; +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProjectReq { + + @Schema(description = "프로젝트 이름", example = "개발새발") + private String title; + + @Schema(description = "프로젝트 한 줄 정보", example = "토이 프로젝트를 공유할 수 있는 사이트입니다") + private String introduction; + + @Schema(description = "프로젝트 설명", example = "토이 프로젝트를 공유할 수 있는 사이트입니다 SpringBoot와 Next.js를 사용했습니다") + private String content; + + @Schema(description = "프로젝트 url", example = "https://www.google.com") + private String url; + + @Schema(description = "프로젝트 공지사항", example = "방금 막 완성했습니다") + private String notice; + + @Schema(description = "프로젝트 id(버전 업데이트가 아니면 -1)", example = "-1") + private Long projectId; + + @Schema(description = "선택한 tag의 id", example = "[1, 2, 3]") + private List selectedTagsId; + + @Schema(description = "선택한 feedback id", example = "[1, 2, 3]") + private List selectedFeedbacksId; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/Feedbacks.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/Feedbacks.java new file mode 100644 index 00000000..4d774a71 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/Feedbacks.java @@ -0,0 +1,54 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.util.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "feedbacks") +public class Feedbacks extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedbacks_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long feedbacksId; + + @Builder.Default + @Column(name = "content", nullable = true, columnDefinition = "text") + private String content = ""; + + @Builder.Default + @Column(name = "like_cnt", nullable = true) + private int likeCnt = 0; + + @Builder.Default + @Column(name = "complaint", nullable = true) + private int complaint = 0; + + @Builder.Default + @Column(name = "selected", nullable = true) + private int selected = 0; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "projects_id", nullable = false) + private Projects projects; + + @Builder.Default + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id") + private Users users = null; + + public void selectedUp() { + this.selected += 1; + } + + public void selectedDown() { + this.selected -= 1; + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectFavorites.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectFavorites.java new file mode 100644 index 00000000..c673e6dd --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectFavorites.java @@ -0,0 +1,32 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.user.entity.Users; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "project_favorites") +public class ProjectFavorites { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "favorites_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long favoritesId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id", nullable = false) + private Users users; + + @Builder.Default + @Column(name = "content", nullable = true, columnDefinition = "text") + private String content = ""; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "projects_id", nullable = false) + private Projects projects; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectId.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectId.java new file mode 100644 index 00000000..86aa383c --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectId.java @@ -0,0 +1,44 @@ +package com.cody.roughcode.project.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import java.io.Serializable; +import java.util.Objects; + +@Builder +@AllArgsConstructor +@RequiredArgsConstructor +@Getter +@Embeddable +public class ProjectId implements Serializable { + + @Column(name = "projects_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long projectId; + + @Column(name = "version") + private int version; + + // equals 메서드 구현 + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProjectId)) return false; + ProjectId that = (ProjectId) o; + return Objects.equals(projectId, that.projectId) && + version == that.version; + } + + // hashCode 메서드 구현 + @Override + public int hashCode() { + return Objects.hash(projectId, version); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectSelectedTags.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectSelectedTags.java new file mode 100644 index 00000000..5f8f426b --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectSelectedTags.java @@ -0,0 +1,28 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.code.entity.Codes; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "project_selected_tags") +public class ProjectSelectedTags { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "selected_tags_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long selectedTagsId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tags_id", nullable = false) + private ProjectTags tags; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "projects_id", nullable = false) + private Projects projects; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectTags.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectTags.java new file mode 100644 index 00000000..8d7326a0 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectTags.java @@ -0,0 +1,32 @@ +package com.cody.roughcode.project.entity; + +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "project_tags") +public class ProjectTags { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tags_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long tagsId; + + @Column(name = "name", length = 255, nullable = false) + private String name; + + @Builder.Default + @Column(name = "cnt", nullable = true) + private int cnt = 0; + + public void cntUp() { + this.cnt += 1; + } + + public void cntDown() { this.cnt -= 1; } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/Projects.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/Projects.java new file mode 100644 index 00000000..ca60a291 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/Projects.java @@ -0,0 +1,88 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.code.entity.Codes; +import com.cody.roughcode.project.dto.req.ProjectReq; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.util.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; +import java.util.List; +import java.util.List; +import java.util.Objects; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "projects") +public class Projects extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "projects_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long projectsId; + + @Column(name = "num", nullable = false) + private Long num; + + @Column(name = "version", nullable = false) + private int version; + + @Column(name = "title", length = 20, nullable = false) + private String title; + + @Builder.Default + @Column(name = "like_cnt", nullable = true) + private int likeCnt = 0; + + @Builder.Default + @Column(name = "review_cnt", nullable = true) + private int reviewCnt = 0; + + @Column(name = "img", length = 255, nullable = false) + private String img; + + @Builder.Default + @Column(name = "closed", nullable = true) + private boolean closed = false; + + @Column(name = "introduction", nullable = false) + private String introduction; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_writer_id", nullable = false) + private Users projectWriter; + + @OneToMany(mappedBy = "projects") + private List selectedTags; + + @OneToMany(mappedBy = "projects") + private List selectedFeedbacks; + + @OneToMany(mappedBy = "projects") + private List projectsCodes; + + public void updateProject(ProjectReq req) { + this.title = req.getTitle(); + this.introduction = req.getIntroduction(); + } + + public void setImgUrl(String imgUrl) { + this.img = imgUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Projects projects = (Projects) o; + return version == projects.version && projectsId.equals(projects.projectsId) && num.equals(projects.num) && projectWriter.equals(projects.projectWriter); + } + + @Override + public int hashCode() { + return Objects.hash(projectsId, num, version, projectWriter); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectsInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectsInfo.java new file mode 100644 index 00000000..24ee2d9b --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/ProjectsInfo.java @@ -0,0 +1,56 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.code.entity.Codes; +import com.cody.roughcode.project.dto.req.ProjectReq; +import lombok.*; + +import javax.persistence.*; +import java.util.List; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "projects_info") +public class ProjectsInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long id; + + @Column(columnDefinition = "text") + private String content; + + @Column(name = "url", length = 255, nullable = false) + private String url; + + @Builder.Default + @Column(name = "complaint", nullable = true) + private int complaint = 0; + + @Column(name = "notice", nullable = false, columnDefinition = "text") + private String notice; + + @Builder.Default + @Column(name = "favorite_cnt", nullable = true) + private int favoriteCnt = 0; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "projects_id", nullable = false) + private Projects projects; + + @OneToMany(mappedBy = "projects") + private List projectsFeedbacks; + + public void setProjects(Projects projects) { + this.projects = projects; + } + + public void updateProject(ProjectReq req) { + this.content = req.getContent(); + this.url = req.getUrl(); + this.notice = req.getNotice(); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/SelectedFeedbacks.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/SelectedFeedbacks.java new file mode 100644 index 00000000..c4a7e99f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/entity/SelectedFeedbacks.java @@ -0,0 +1,28 @@ +package com.cody.roughcode.project.entity; + +import com.cody.roughcode.code.entity.Codes; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "selected_feedbacks") +public class SelectedFeedbacks { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "selected_feedbacks_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long selectedTagsId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feedbacks_id", nullable = false) + private Feedbacks feedbacks; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "selected_project_id", nullable = false) + private Projects projects; +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/FeedbacksRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/FeedbacksRepository.java new file mode 100644 index 00000000..00710179 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/FeedbacksRepository.java @@ -0,0 +1,8 @@ +package com.cody.roughcode.project.repository; + +import com.cody.roughcode.project.entity.Feedbacks; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FeedbacksRepository extends JpaRepository { + Feedbacks findFeedbacksByFeedbacksId(Long id); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectSelectedTagsRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectSelectedTagsRepository.java new file mode 100644 index 00000000..acf0c71f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectSelectedTagsRepository.java @@ -0,0 +1,7 @@ +package com.cody.roughcode.project.repository; + +import com.cody.roughcode.project.entity.ProjectSelectedTags; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProjectSelectedTagsRepository extends JpaRepository { +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectTagsRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectTagsRepository.java new file mode 100644 index 00000000..b84aae75 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectTagsRepository.java @@ -0,0 +1,8 @@ +package com.cody.roughcode.project.repository; + +import com.cody.roughcode.project.entity.ProjectTags; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProjectTagsRepository extends JpaRepository { + ProjectTags findByTagsId(Long id); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectsInfoRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectsInfoRepository.java new file mode 100644 index 00000000..eeb9de81 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectsInfoRepository.java @@ -0,0 +1,9 @@ +package com.cody.roughcode.project.repository; + +import com.cody.roughcode.project.entity.Projects; +import com.cody.roughcode.project.entity.ProjectsInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProjectsInfoRepository extends JpaRepository { + ProjectsInfo findByProjects(Projects project); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectsRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectsRepository.java new file mode 100644 index 00000000..250614b1 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/ProjectsRepository.java @@ -0,0 +1,16 @@ +package com.cody.roughcode.project.repository; + +import com.amazonaws.services.s3.transfer.Copy; +import com.cody.roughcode.project.entity.Projects; +import com.cody.roughcode.user.entity.Users; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +public interface ProjectsRepository extends JpaRepository { + Projects findByProjectsId(Long id); + + @Query(value = "SELECT p FROM Projects p WHERE p.num = :num AND p.version = (SELECT MAX(p2.version) FROM Projects p2 WHERE p2.num = :num AND p2.projectWriter.usersId = :userId) AND p.projectWriter.usersId = :userId") + Projects findLatestProject(@Param("num") Long num, @Param("userId") Long userId); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/SelectedFeedbacksRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/SelectedFeedbacksRepository.java new file mode 100644 index 00000000..f929bd14 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/repository/SelectedFeedbacksRepository.java @@ -0,0 +1,7 @@ +package com.cody.roughcode.project.repository; + +import com.cody.roughcode.project.entity.SelectedFeedbacks; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SelectedFeedbacksRepository extends JpaRepository { +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/ProjectsService.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/ProjectsService.java new file mode 100644 index 00000000..dfd8138f --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/ProjectsService.java @@ -0,0 +1,10 @@ +package com.cody.roughcode.project.service; + +import com.cody.roughcode.project.dto.req.ProjectReq; +import org.springframework.web.multipart.MultipartFile; + +public interface ProjectsService { + Long insertProject(ProjectReq req, Long usersId); + int updateProjectThumbnail(MultipartFile thumbnail, Long projectsId, Long usersId); + int updateProject(ProjectReq req, Long usersId); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/ProjectsServiceImpl.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/ProjectsServiceImpl.java new file mode 100644 index 00000000..946be103 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/ProjectsServiceImpl.java @@ -0,0 +1,239 @@ +package com.cody.roughcode.project.service; + +import com.cody.roughcode.code.repository.CodesRepostiory; +import com.cody.roughcode.exception.NotMatchException; +import com.cody.roughcode.exception.NotNewestVersionException; +import com.cody.roughcode.exception.SaveFailedException; +import com.cody.roughcode.exception.UpdateFailedException; +import com.cody.roughcode.project.dto.req.ProjectReq; +import com.cody.roughcode.project.entity.*; +import com.cody.roughcode.project.repository.*; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.user.repository.UsersRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ProjectsServiceImpl implements ProjectsService{ + + private final S3FileServiceImpl s3FileService; + + private final UsersRepository usersRepository; + private final ProjectsRepository projectsRepository; + private final ProjectsInfoRepository projectsInfoRepository; + private final ProjectSelectedTagsRepository projectSelectedTagsRepository; + private final ProjectTagsRepository projectTagsRepository; + private final CodesRepostiory codesRepository; + private final FeedbacksRepository feedbacksRepository; + private final SelectedFeedbacksRepository selectedFeedbacksRepository; + + @Override + @Transactional + public Long insertProject(ProjectReq req, Long usersId) { + Users user = usersRepository.findByUsersId(usersId); + if(user == null) throw new NullPointerException("일치하는 유저가 존재하지 않습니다"); + ProjectsInfo info = ProjectsInfo.builder() + .url(req.getUrl()) + .notice(req.getNotice()) + .build(); + + // 새 프로젝트를 생성하는거면 projectNum은 작성자의 projects_cnt + 1 + // 전의 프로젝트를 업데이트하는거면 projectNum은 전의 projectNum과 동일 + Long projectNum; + int projectVersion; + int likeCnt = 0; + if(req.getProjectId() == -1){ // 새 프로젝트 생성 + user.projectsCntUp(); + usersRepository.save(user); + + projectNum = user.getProjectsCnt(); + projectVersion = 1; + } else { // 기존 프로젝트 버전 업 + // num 가져오기 + // num과 user가 일치하는 max version값 가져오기 + // num과 user와 max version값에 일치하는 project 가져오기 + Projects original = projectsRepository.findByProjectsId(req.getProjectId()); + if(original == null) throw new NullPointerException("일치하는 프로젝트가 존재하지 않습니다"); + original = projectsRepository.findLatestProject(original.getNum(), user.getUsersId()); + if(original == null) throw new NullPointerException("일치하는 프로젝트가 존재하지 않습니다"); + + if(!original.getProjectWriter().equals(user)) throw new NotMatchException(); + + projectNum = original.getNum(); + projectVersion = original.getVersion() + 1; + likeCnt = original.getLikeCnt(); + } + + Long projectId = -1L; + try { + Projects project = Projects.builder() + .num(projectNum) + .version(projectVersion) + .img("temp") + .introduction(req.getIntroduction()) + .title(req.getTitle()) + .projectWriter(user) + .likeCnt(likeCnt) + .build(); + Projects savedProject = projectsRepository.save(project); + projectId = savedProject.getProjectsId(); + + // tag 등록 + if(req.getSelectedTagsId() != null) + for(Long id : req.getSelectedTagsId()){ + ProjectTags projectTag = projectTagsRepository.findByTagsId(id); + projectSelectedTagsRepository.save(ProjectSelectedTags.builder() + .tags(projectTag) + .projects(project) + .build()); + + projectTag.cntUp(); + projectTagsRepository.save(projectTag); + } + else log.info("등록한 태그가 없습니다"); + + // feedback 선택 + if(req.getSelectedFeedbacksId() != null) + for(Long id : req.getSelectedFeedbacksId()){ + Feedbacks feedback = feedbacksRepository.findFeedbacksByFeedbacksId(id); + if(feedback == null) throw new NullPointerException("일치하는 피드백이 없습니다"); + if(!feedback.getProjects().getNum().equals(projectNum)) + throw new NullPointerException("피드백과 프로젝트가 일치하지 않습니다"); + feedback.selectedUp(); + feedbacksRepository.save(feedback); + + SelectedFeedbacks selectedFeedback = SelectedFeedbacks.builder() + .feedbacks(feedback) + .projects(savedProject) + .build(); + selectedFeedbacksRepository.save(selectedFeedback); + } + else log.info("선택한 피드백이 없습니다"); + + info.setProjects(savedProject); + projectsInfoRepository.save(info); + } catch(Exception e){ + log.error(e.getMessage()); + throw new SaveFailedException(e.getMessage()); + } + + return projectId; + } + + @Override + @Transactional + public int updateProjectThumbnail(MultipartFile thumbnail, Long projectsId, Long usersId) { + Users user = usersRepository.findByUsersId(usersId); + if(user == null) throw new NullPointerException("일치하는 유저가 존재하지 않습니다"); + if(thumbnail == null) throw new NullPointerException("썸네일이 등록되어있지 않습니다"); + Projects project = projectsRepository.findByProjectsId(projectsId); + if(project == null) throw new NullPointerException("일치하는 프로젝트가 없습니다"); + if(!project.getProjectWriter().equals(user)) throw new NotMatchException(); + Projects latestProject = projectsRepository.findLatestProject(project.getNum(), usersId); + if(!project.equals(latestProject)) throw new NotNewestVersionException(); + + Long projectNum = project.getNum(); + int projectVersion = project.getVersion(); + + try{ + String fileName = user.getName() + "_" + projectNum + "_" + projectVersion; + + String imgUrl = s3FileService.upload(thumbnail, "project", fileName); + + project.setImgUrl(imgUrl); + projectsRepository.save(project); + } catch(Exception e){ + log.error(e.getMessage()); + throw new SaveFailedException(e.getMessage()); + } + + return 1; + } + + @Override + public int updateProject(ProjectReq req, Long usersId) { + Users user = usersRepository.findByUsersId(usersId); + if(user == null) throw new NullPointerException("일치하는 유저가 존재하지 않습니다"); + + // 기존의 프로젝트 가져오기 + Projects target = projectsRepository.findByProjectsId(req.getProjectId()); + if(target == null) throw new NullPointerException("일치하는 프로젝트가 존재하지 않습니다"); + Projects latestProject = projectsRepository.findLatestProject(target.getNum(), user.getUsersId()); + if(!target.equals(latestProject)) throw new NotNewestVersionException(); + + ProjectsInfo originalInfo = projectsInfoRepository.findByProjects(target); + if(originalInfo == null) throw new NullPointerException("일치하는 프로젝트가 존재하지 않습니다"); + + try { + // tag 삭제 + List selectedTagsList = target.getSelectedTags(); + if(selectedTagsList != null) + for (ProjectSelectedTags tag : selectedTagsList) { + ProjectTags projectTag = tag.getTags(); + projectTag.cntDown(); + projectTagsRepository.save(projectTag); + + projectSelectedTagsRepository.delete(tag); + } + else log.info("기존에 선택하였던 tag가 없습니다"); + + // tag 등록 + if(req.getSelectedTagsId() != null) + for(Long id : req.getSelectedTagsId()){ + ProjectTags projectTag = projectTagsRepository.findByTagsId(id); + projectSelectedTagsRepository.save(ProjectSelectedTags.builder() + .tags(projectTag) + .projects(target) + .build()); + + projectTag.cntUp(); + projectTagsRepository.save(projectTag); + } + else log.info("새로 선택한 tag가 없습니다"); + + // feedback 삭제 + List selectedFeedbacksList = target.getSelectedFeedbacks(); + if(selectedFeedbacksList != null) + for (SelectedFeedbacks feedback : selectedFeedbacksList) { + Feedbacks feedbacks = feedback.getFeedbacks(); + feedbacks.selectedDown(); + feedbacksRepository.save(feedbacks); + + selectedFeedbacksRepository.delete(feedback); + } + else log.info("기존에 선택하였던 feedback이 없습니다"); + + // feedback 등록 + if(req.getSelectedFeedbacksId() != null) + for(Long id : req.getSelectedFeedbacksId()){ + Feedbacks feedbacks = feedbacksRepository.findFeedbacksByFeedbacksId(id); + selectedFeedbacksRepository.save(SelectedFeedbacks.builder() + .projects(target) + .feedbacks(feedbacks) + .build()); + + feedbacks.selectedUp(); + feedbacksRepository.save(feedbacks); + } + else log.info("새로 선택한 feedback이 없습니다"); + + target.updateProject(req); // title, introduction 업데이트 + originalInfo.updateProject(req); + } catch(Exception e){ + log.error(e.getMessage()); + throw new UpdateFailedException(e.getMessage()); + } + + return 1; + } + +} + + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/S3FileService.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/S3FileService.java new file mode 100644 index 00000000..014f96ca --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/S3FileService.java @@ -0,0 +1,11 @@ +package com.cody.roughcode.project.service; + +import com.cody.roughcode.project.dto.req.ProjectReq; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface S3FileService { + String upload(MultipartFile profile, String dirName, String fileName) throws Exception; + boolean delete(String imageUrlString); +} \ No newline at end of file diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/S3FileServiceImpl.java b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/S3FileServiceImpl.java new file mode 100644 index 00000000..0e7bb6c0 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/project/service/S3FileServiceImpl.java @@ -0,0 +1,104 @@ +package com.cody.roughcode.project.service; + +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.*; +import com.cody.roughcode.exception.DeletionFailException; +import com.cody.roughcode.project.dto.req.ProjectReq; +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.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.nio.file.Paths; + +@Slf4j +@RequiredArgsConstructor +@Component +public class S3FileServiceImpl implements S3FileService { + + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + // 이미지 업로드 후 URL 리턴 + @Override + public String upload(MultipartFile multipartFile, String dirName, String fileName) throws IOException { + log.info("-----------upload method start-----------"); + log.info("file : {}, dirName : {}", multipartFile, dirName); + + // 파일 변환 + File uploadFile = convertToFile(multipartFile) + .orElseThrow(() -> new IllegalArgumentException("MultipartFile에서 File로 변환에 실패했습니다")); + + // 파일명에 project 정보 같이 입력 + StringBuilder fileInfo = new StringBuilder(dirName + "/" + fileName); + + log.info("new file Name : {}", fileInfo); + + // S3로 업로드 + amazonS3Client.putObject(new PutObjectRequest(bucket, String.valueOf(fileInfo), uploadFile) + .withCannedAcl(CannedAccessControlList.PublicRead)); + + URL imageUrl = amazonS3Client.getUrl(bucket, String.valueOf(fileInfo)); + if (imageUrl == null) { + throw new NullPointerException("이미지 저장에 실패했습니다"); + } + String imageUrlString = imageUrl.toString(); + + // 로컬 파일 삭제 + if (uploadFile.exists()) { + if (uploadFile.delete()) { + log.info("로컬에서 파일이 삭제 성공"); + } else { + log.error("로컬에서 파일이 삭제 실패"); + } + } + + log.info("return : {}", imageUrlString); + + return imageUrlString; + } + + // multipartFile -> File 형식으로 변환 및 로컬에 저장 + private Optional convertToFile(MultipartFile file) throws IOException { + File uploadFile = new File(Objects.requireNonNull(file.getOriginalFilename())); + FileOutputStream fos = new FileOutputStream(uploadFile); + fos.write(file.getBytes()); + fos.close(); + + return Optional.of(uploadFile); + } + + // 이미지 삭제 method + public boolean delete(String imageUrlString) { + try { + URL imageUrl = new URL(imageUrlString); + String key = imageUrl.getPath().substring(1); + amazonS3Client.deleteObject(bucket, key); + } catch (Exception e) { + log.error(e.getMessage()); + throw new DeletionFailException("이미지"); + } + + return true; + } + +} \ No newline at end of file diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtAuthenticationFilter.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtAuthenticationFilter.java new file mode 100644 index 00000000..e78fd09c --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtAuthenticationFilter.java @@ -0,0 +1,38 @@ +package com.cody.roughcode.security.auth; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@Log4j2 +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + // Cookie 에서 JWT Token 추출 + String token = jwtTokenProvider.getAccessToken(request); + if (!((HttpServletRequest) request).getRequestURI().equals("/tokens/reissue")) { + //재발급 요청이 아니라면 + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug(authentication.getName() + "의 인증정보 저장"); + } else { + log.debug("유효한 JWT 토큰이 없습니다"); + } + } + + chain.doFilter(request, response); + } + +} + + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtExceptionFilter.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtExceptionFilter.java new file mode 100644 index 00000000..d889f807 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtExceptionFilter.java @@ -0,0 +1,45 @@ +package com.cody.roughcode.security.auth; + +import com.cody.roughcode.exception.NoTokenException; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.SignatureException; +import org.springframework.http.MediaType; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class JwtExceptionFilter extends GenericFilter { + + + public JwtExceptionFilter() { + } + + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + chain.doFilter(request, response); //jwtauthenticaionfilter + } catch (NoTokenException e) { + setErrorResponse(response, "토큰이 없습니다"); + } catch (ExpiredJwtException e) { + setErrorResponse(response, "토큰이 만료되었습니다"); + } catch (MalformedJwtException e) { + setErrorResponse(response, "손상된 토큰입니다"); + } catch (UnsupportedJwtException e) { + setErrorResponse(response, "지원하지 않는 토큰입니다"); + } catch (SignatureException e) { + setErrorResponse(response, "시그니처 검증에 실패한 토큰입니다"); + } catch (IllegalArgumentException e) { + setErrorResponse(response, "토큰에 해당하는 유저가 없습니다"); + } + } + + private void setErrorResponse(ServletResponse response, String error) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, error); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtProperties.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtProperties.java new file mode 100644 index 00000000..71530709 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtProperties.java @@ -0,0 +1,13 @@ +package com.cody.roughcode.security.auth; + +//@Configuration + +public interface JwtProperties { + String TOKEN_HEADER = "Authorization"; + int ACCESS_TOKEN_TIME = 30 * 1000 * 60; // 30분 + int REFRESH_TOKEN_TIME = 7 * 24 * 60 * 60 * 1000; // 7일 + String AUTHORITIES_KEY = "auth"; + String REFRESH_TOKEN = "refreshToken"; + String ACCESS_TOKEN = "accessToken"; +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtTokenProvider.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtTokenProvider.java new file mode 100644 index 00000000..b7bb2725 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/JwtTokenProvider.java @@ -0,0 +1,135 @@ +package com.cody.roughcode.security.auth; + +import com.cody.roughcode.security.dto.TokenReq; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.user.repository.UsersRepository; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletRequest; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Log4j2 +@Component +public class JwtTokenProvider { + + private final UsersRepository usersRepository; + private final Key key; + + public JwtTokenProvider(UsersRepository usersRepository, @Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + this.usersRepository = usersRepository; + } + + /** + * // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메서드 + */ + public TokenInfo generateToken(Authentication authentication) { + // 권한 가져오기 + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + Users user = usersRepository.findByName(authentication.getName()).orElseThrow(); + long now = new Date().getTime(); + + // Access Token 생성 + Date accessTokenExpiresIn = new Date(now + JwtProperties.ACCESS_TOKEN_TIME); + String accessToken = Jwts.builder() + .claim("id", user.getUsersId()) + .claim("name", user.getName()) + .claim(JwtProperties.AUTHORITIES_KEY, authorities) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + // Refresh Token 생성 + String refreshToken = Jwts.builder() + .setExpiration(new Date(now + JwtProperties.REFRESH_TOKEN_TIME)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + return new TokenInfo(accessToken, refreshToken, user.getUsersId()); + } + + // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드 + public Authentication getAuthentication(String accessToken) { + // 토큰 복호화 + Claims claims = parseClaims(accessToken); + + if (claims.get(JwtProperties.AUTHORITIES_KEY) == null) { + throw new MalformedJwtException("손상된 토큰입니다"); + } + + // 클레임에서 권한 정보 가져오기 + Collection authorities = + Arrays.stream(claims.get(JwtProperties.AUTHORITIES_KEY).toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + // UserDetails 객체를 만들어서 Authentication 리턴 + Users user = usersRepository.findById(Long.parseLong(claims.get("id").toString())).orElseThrow(IllegalArgumentException::new); + UserDetailsCustom principal = new UserDetailsCustom(user); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + // 토큰 정보를 검증하는 메서드 + public boolean validateToken(String token) { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } + + // Access Token 만료시 갱신 때 사용할 정보를 얻기 위해 Claim 리턴함 + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + public Long getId(String token) { + return Long.parseLong(parseClaims(token).get("id").toString()); + } + + public TokenReq getToken(HttpServletRequest request) { + TokenReq tokenReq = new TokenReq(); + for (Cookie cookie : request.getCookies()) { + String name = cookie.getName(); + if (name.equals("accessToken")) { + tokenReq.setAccessToken(cookie.getValue()); + } else if (name.equals("refreshToken")) { + tokenReq.setRefreshToken(cookie.getValue()); + } + } + return tokenReq; + } + + public String getAccessToken(ServletRequest request) { + if (((HttpServletRequest) request).getCookies() != null) { + for (Cookie cookie : ((HttpServletRequest) request).getCookies()) { + String name = cookie.getName(); + if (name.equals("accessToken")) { + return cookie.getValue(); + } + } + } + return null; + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/TokenInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/TokenInfo.java new file mode 100644 index 00000000..09886a1a --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/TokenInfo.java @@ -0,0 +1,43 @@ +package com.cody.roughcode.security.auth; + +import lombok.Data; +import org.springframework.http.ResponseCookie; + +@Data +public class TokenInfo { + private final String accessToken; + private final String refreshToken; + + private final Long userId; + + public TokenInfo(String accessToken, String refreshToken, Long userId) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.userId = userId; + } + + public ResponseCookie generateAccessToken() { + return ResponseCookie + .from(JwtProperties.ACCESS_TOKEN, this.accessToken) +// .domain("localhost") + .path("/") + .maxAge(JwtProperties.ACCESS_TOKEN_TIME) + .httpOnly(true) + .sameSite("Lax") + .secure(false) // 문제 발생 예정 + .build(); + } + + public ResponseCookie generateRefreshToken() { + return ResponseCookie + .from(JwtProperties.REFRESH_TOKEN, this.refreshToken) +// .domain("localhost") + .path("/") + .maxAge(JwtProperties.REFRESH_TOKEN_TIME) + .httpOnly(true) + .sameSite("Lax") + .secure(false) + .build(); + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/UserDetailsCustom.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/UserDetailsCustom.java new file mode 100644 index 00000000..72a85c3b --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/auth/UserDetailsCustom.java @@ -0,0 +1,46 @@ +package com.cody.roughcode.security.auth; + +import com.cody.roughcode.user.entity.Users; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class UserDetailsCustom implements OAuth2User { + private final Users user; + private Map attributes; + + public UserDetailsCustom(Users user, Map attributes) { + this.user = user; + this.attributes = attributes; + } + + public UserDetailsCustom(Users user) { + this.user = user; + } + + @Override + public String getName() { + return user.getName(); + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + @Override + public Collection getAuthorities() { + List list = new ArrayList<>(); + for(String role: user.getRoles()){ + list.add(new SimpleGrantedAuthority(role)); + } + return list; + } + +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/dto/TokenReq.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/dto/TokenReq.java new file mode 100644 index 00000000..adfc87d7 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/dto/TokenReq.java @@ -0,0 +1,16 @@ +package com.cody.roughcode.security.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class TokenReq { + private String accessToken; + private String refreshToken; + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationFailureHandler.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationFailureHandler.java new file mode 100644 index 00000000..f8056bfa --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationFailureHandler.java @@ -0,0 +1,23 @@ +package com.cody.roughcode.security.handler; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationSuccessHandler.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationSuccessHandler.java new file mode 100644 index 00000000..38cb4a73 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationSuccessHandler.java @@ -0,0 +1,91 @@ +package com.cody.roughcode.security.handler; + +import com.cody.roughcode.security.auth.JwtProperties; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.security.auth.TokenInfo; +import com.cody.roughcode.security.oauth2.CookieOAuth2AuthorizationRequestRepository; +import com.cody.roughcode.exception.BadRequestException; +import com.cody.roughcode.util.CookieUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static com.cody.roughcode.security.oauth2.CookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +@Log4j2 +@Component +@RequiredArgsConstructor +public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + @Value("${app.oauth2.authorizedRedirectUri}") + private String redirectUri; + private final JwtTokenProvider jwtTokenProvider; + private final RedisTemplate redisTemplate; + private final CookieOAuth2AuthorizationRequestRepository authorizationRequestRepository; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { + String targetUrl = determineTargetUrl(request, response, authentication); + + // JWT 생성 + TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication); // tokenInfo 생성 + + // redis 저장 + redisTemplate.opsForValue() + .set(tokenInfo.getUserId().toString(), tokenInfo.getRefreshToken(), JwtProperties.REFRESH_TOKEN_TIME, TimeUnit.MILLISECONDS); + // access token, refresh token 쿠키에 저장 + response.addHeader("Set-Cookie", tokenInfo.generateAccessToken().toString()); + response.addHeader("Set-Cookie", tokenInfo.generateRefreshToken().toString()); + + if(response.isCommitted()){ + log.debug("Response has already been committed"); + return; + } + clearAuthenticationAttributes(request, response); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } + + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + Optional redirectUri = CookieUtil.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue); + + if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) { + throw new BadRequestException("redirect URIs are not matched"); + } + String targetUrl = redirectUri.orElse(getDefaultTargetUrl()); + + return UriComponentsBuilder.fromUriString(targetUrl) +// .queryParam("accessToken", accessToken) + .build().toUriString(); + } + + protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) { + super.clearAuthenticationAttributes(request); + authorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + } + + private boolean isAuthorizedRedirectUri(String uri) { + URI clientRedirectUri = URI.create(uri); + URI authorizedUri = URI.create(redirectUri); + + if (authorizedUri.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) + && authorizedUri.getPort() == clientRedirectUri.getPort()) { + return true; + } + return false; + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationSuccessHandler2.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationSuccessHandler2.java new file mode 100644 index 00000000..a8e8da8e --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/AuthenticationSuccessHandler2.java @@ -0,0 +1,33 @@ +package com.cody.roughcode.security.handler; + +import com.cody.roughcode.security.auth.JwtProperties; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.security.auth.TokenInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class AuthenticationSuccessHandler2 extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + private final RedisTemplate redisTemplate; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication); // tokenInfo 만들어서 + + redisTemplate.opsForValue() + .set(tokenInfo.getUserId().toString(), tokenInfo.getRefreshToken(), JwtProperties.REFRESH_TOKEN_TIME, TimeUnit.MILLISECONDS); + response.addHeader("Set-Cookie", tokenInfo.generateAccessToken().toString()); + response.addHeader("Set-Cookie", tokenInfo.generateRefreshToken().toString()); + } +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/CustomLogoutHandler.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/CustomLogoutHandler.java new file mode 100644 index 00000000..eabb9d41 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/handler/CustomLogoutHandler.java @@ -0,0 +1,33 @@ +package com.cody.roughcode.security.handler; + +import com.cody.roughcode.exception.NoTokenException; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.security.dto.TokenReq; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +@RequiredArgsConstructor +public class CustomLogoutHandler implements LogoutHandler { + + private final RedisTemplate redisTemplate; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + // request에 있는 토큰 정보 추출 + TokenReq tokenReq = jwtTokenProvider.getToken(request); + Long userId = jwtTokenProvider.getId(tokenReq.getAccessToken()); + + // redis 에 해당 정보로 저장된 Refresh token 이 있는지 여부를 확인 후 있다면 삭제 + if (!Boolean.TRUE.equals(redisTemplate.delete(userId.toString()))) { + throw new NoTokenException(); + } + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CookieOAuth2AuthorizationRequestRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CookieOAuth2AuthorizationRequestRepository.java new file mode 100644 index 00000000..99319646 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CookieOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,52 @@ +package com.cody.roughcode.security.oauth2; + +import com.cody.roughcode.util.CookieUtil; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class CookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { + public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; + private static final int COOKIE_EXPIRE_SECONDS = 180; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + return CookieUtil.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(cookie -> CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class)) + .orElse(null); + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + if (authorizationRequest == null) { + CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + return; + } + + CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS); + String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); + + if (StringUtils.isNotBlank(redirectUriAfterLogin)) { + CookieUtil.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS); + } + + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { + return this.loadAuthorizationRequest(request); + } + + public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) { + CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CustomOAuth2AuthorizationRequestRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CustomOAuth2AuthorizationRequestRepository.java new file mode 100644 index 00000000..72001ee3 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CustomOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,107 @@ +package com.cody.roughcode.security.oauth2; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +@Component +public class CustomOAuth2AuthorizationRequestRepository implements + AuthorizationRequestRepository { + + //AuthorizationRequestRepository는 인가 요청을 시작한 시점부터 인가 요청을 받는 시점까지 OAuth2AuthorizationRequest를 유지해줌 + //default는 HttpSession에 저장하는 HttpSessionOAuth2AuthorizationRequestRepository이다 + //HttpSessionOAuth2AuthorizationRequestRepository는 세션을 이용해서 저장을 하는데 + //우리는 프론트로부터 Authorization code를 받기 때문에, 이 과정이 필요없다 + //즉, remove과정만 진행하게 되므로 remove에서 올바른 OAuth2AuthorizationRequest를 반환해주면 된다 + private final ClientRegistrationRepository clientRegistrationRepository; + + public CustomOAuth2AuthorizationRequestRepository( + ClientRegistrationRepository clientRegistrationRepository) { + this.clientRegistrationRepository = clientRegistrationRepository; + } + + //client registration 설정을 가지고 + //RequestResolver의 로직을 따라간다 + private static String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) { + Map uriVariables = new HashMap<>(); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl( + UrlUtils.buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .fragment(null) + .build(); + + String scheme = uriComponents.getScheme(); + uriVariables.put("baseScheme", (scheme != null) ? scheme : ""); + String host = uriComponents.getHost(); + uriVariables.put("baseHost", (host != null) ? host : ""); + // following logic is based on HierarchicalUriComponents#toUriString() + int port = uriComponents.getPort(); + uriVariables.put("basePort", (port == -1) ? "" : ":" + port); + String path = uriComponents.getPath(); + if (org.springframework.util.StringUtils.hasLength(path)) { + if (path.charAt(0) != '/') { + path = '/' + path; + } + } + uriVariables.put("basePath", (path != null) ? path : ""); + uriVariables.put("baseUrl", uriComponents.toUriString()); + uriVariables.put("action", "login"); + return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUri()) + .buildAndExpand(uriVariables) + .toUriString(); + } + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + return null; + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { + Assert.notNull(request, "request cannot be null"); + + OAuth2AuthorizationRequest originalRequest; + + //state에 google과 kakao를 구분할 수 있는 string을 넣어놓았다 올바른 방법이 아니므로 다른 방법을 찾아봐야 한다 + String registrationId = request.getParameter("state"); + ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + } + OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode() + .attributes((attrs) -> + attrs.put(OAuth2ParameterNames.REGISTRATION_ID, + clientRegistration.getRegistrationId())); + + String redirectUriStr = expandRedirectUri(request, clientRegistration); + + builder.clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(redirectUriStr) + .scopes(clientRegistration.getScopes()) + .state(request.getParameter("state")); + + originalRequest = builder.build(); + + return originalRequest; + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CustomOAuth2UserService.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CustomOAuth2UserService.java new file mode 100644 index 00000000..0c147c42 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,84 @@ +package com.cody.roughcode.security.oauth2; + + +import com.cody.roughcode.security.auth.UserDetailsCustom; +import com.cody.roughcode.security.oauth2.provider.GithubUserInfo; +import com.cody.roughcode.security.oauth2.provider.GoogleUserInfo; +import com.cody.roughcode.security.oauth2.provider.KaKaoUserInfo; +import com.cody.roughcode.security.oauth2.provider.OAuth2UserInfo; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.user.enums.Role; +import com.cody.roughcode.user.repository.UsersRepository; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + private final UsersRepository usersRepository; + + public CustomOAuth2UserService(UsersRepository usersRepository) { + this.usersRepository = usersRepository; + } + + // 깃허브로부터 받은 userRequest 데이터에 대한 후처리되는 함수 + // 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다 + // OAuthAttributes: OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스 + // UsersRepository: 엔티티 클래스를 DB에 접근하게 해주는 인터페이스 + // CustomOAuth2UserService: 깃허브 로그인 이후 가져온 사용자의 정보(email, name 등)들을 기반으로 가입 및 정보수정 등의 기능 지원 + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + return processOAuth2User(userRequest, oAuth2User); + } + + private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) { + OAuth2UserInfo oAuth2UserInfo; + if (userRequest.getClientRegistration().getRegistrationId().equals("github")) { + System.out.println(oAuth2User.getAttributes()); + //깃허브 로그인 요청 + oAuth2UserInfo = new GithubUserInfo(oAuth2User.getAttributes()); + } else if (userRequest.getClientRegistration().getRegistrationId().equals("google")) { + //구글 로그인 요청 + oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes()); + } else if (userRequest.getClientRegistration().getRegistrationId().equals("kakao")) { + //카카오 로그인 요청 + oAuth2UserInfo = new KaKaoUserInfo(oAuth2User.getAttributes()); + } else { + //다른 소셜 로그인 요청 + return null; + } + //ex)kakao_1238471249 +// String username = oAuth2UserInfo.getProvider() + '_' + oAuth2UserInfo.getProviderId(); + String name = oAuth2UserInfo.getName(); + String email = oAuth2UserInfo.getEmail(); + + // 이미 가입되어있는지 찾아봄 + // DB에 해당 유저가 있으면 유저를 바로 반환 + Optional userOptional = + usersRepository.findByName(name); + + // DB에 해당 유저가 없으면 새로 만들어줌. + // 닉네임은 해당 유저의 깃허브 아이디로, 이메일은 깃허브 연동 이메일을 넣어줌 + List roles = new ArrayList<>(); + roles.add("ROLE_USER"); + Users user = userOptional.orElseGet(() -> + usersRepository.save( + Users.builder() + .name(name) + .email(email) + .roles(roles) + .build() + )); + return new UserDetailsCustom(user, oAuth2User.getAttributes()); + } +} + diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/GithubUserInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/GithubUserInfo.java new file mode 100644 index 00000000..94a3bf92 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/GithubUserInfo.java @@ -0,0 +1,35 @@ +package com.cody.roughcode.security.oauth2.provider; + +import java.util.Map; + +public class GithubUserInfo implements OAuth2UserInfo { + + private final Map attributes; + + public GithubUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getProviderId() { + return attributes.get("id").toString(); + } + + @Override + public String getProvider() { + return "github"; + } + + @Override + public String getName() { + return attributes.get("login").toString(); + } + + @Override + public String getEmail() { + // github의 경우 public으로 설정한 이메일이 없다면 null로 넘어옴 + // email이 null이라면 빈 문자열 넣기 + return (attributes.get("email") == null) ? "" : attributes.get("email").toString(); + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/GoogleUserInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/GoogleUserInfo.java new file mode 100644 index 00000000..453cc291 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/GoogleUserInfo.java @@ -0,0 +1,33 @@ +package com.cody.roughcode.security.oauth2.provider; + +import java.util.Map; + +public class GoogleUserInfo implements OAuth2UserInfo { + + private final Map attributes; + + public GoogleUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getProviderId() { + return attributes.get("sub").toString(); + } + + @Override + public String getProvider() { + return "google"; + } + + @Override + public String getName() { + return attributes.get("name").toString(); + } + + @Override + public String getEmail() { + return attributes.get("email").toString(); + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/KaKaoUserInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/KaKaoUserInfo.java new file mode 100644 index 00000000..f98e027e --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/KaKaoUserInfo.java @@ -0,0 +1,36 @@ +package com.cody.roughcode.security.oauth2.provider; + +import java.util.Map; + +public class KaKaoUserInfo implements OAuth2UserInfo { + + private final Map attributes; + private final Map properties; + private final Map kakao_account; + + public KaKaoUserInfo(Map attributes) { + this.attributes = attributes; + properties = (Map) attributes.get("properties"); + kakao_account = (Map) attributes.get("kakao_account"); + } + + @Override + public String getProviderId() { + return attributes.get("id").toString(); + } + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getName() { + return properties.get("nickname"); + } + + @Override + public String getEmail() { + return kakao_account.get("email"); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/OAuth2UserInfo.java b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/OAuth2UserInfo.java new file mode 100644 index 00000000..9349d2a3 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/security/oauth2/provider/OAuth2UserInfo.java @@ -0,0 +1,12 @@ +package com.cody.roughcode.security.oauth2.provider; + +public interface OAuth2UserInfo { + + String getProviderId(); + + String getProvider(); + + String getName(); + + String getEmail(); +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/controller/UsersController.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/controller/UsersController.java new file mode 100644 index 00000000..22333eaf --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/controller/UsersController.java @@ -0,0 +1,41 @@ +package com.cody.roughcode.user.controller; + +import com.cody.roughcode.security.auth.JwtProperties; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.user.dto.res.UserResp; +import com.cody.roughcode.user.service.UsersService; +import com.cody.roughcode.util.Response; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/user") +@RequiredArgsConstructor +@Slf4j +public class UsersController { + + private final JwtTokenProvider jwtTokenProvider; + private final UsersService usersService; + + @GetMapping + public ResponseEntity selectOneUser(@CookieValue(name = JwtProperties.ACCESS_TOKEN) String accessToken) { + Long userId = jwtTokenProvider.getId(accessToken); + + UserResp resp = null; + try{ + resp = usersService.selectOneUser(userId); + } catch (Exception e){ + log.error(e.getMessage()); + } + +// if(res == 0) return Response.notFound("사용자 정보 조회 실패"); + return Response.makeResponse(HttpStatus.OK, "사용자 정보 조회 성공", 1, resp); + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/dto/res/UserResp.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/dto/res/UserResp.java new file mode 100644 index 00000000..610b5b62 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/dto/res/UserResp.java @@ -0,0 +1,23 @@ +package com.cody.roughcode.user.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserResp { + + @Schema(description = "사용자 닉네임", example = "cody306") + private String name; + + @Schema(description = "사용자 이메일(이메일이 없는 경우 빈 문자열)", example = "cody306@ssafy.com") + private String email; + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/entity/Users.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/entity/Users.java new file mode 100644 index 00000000..fae8dc43 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/entity/Users.java @@ -0,0 +1,68 @@ +package com.cody.roughcode.user.entity; + +import com.cody.roughcode.code.entity.Codes; +import com.cody.roughcode.code.entity.Reviews; +import com.cody.roughcode.project.entity.CodeFavorites; +import com.cody.roughcode.project.entity.Feedbacks; +import com.cody.roughcode.project.entity.ProjectFavorites; +import com.cody.roughcode.project.entity.Projects; +import com.cody.roughcode.util.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.List; +import java.util.Objects; + +@Entity +@Getter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "users") +public class Users extends BaseTimeEntity{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "users_id", nullable = false, columnDefinition = "BIGINT UNSIGNED") + private Long usersId; + + @Column(name = "email", length = 255, nullable = false) + private String email; + + @Column(name = "name", length = 30, nullable = false) + private String name; + + @Builder.Default + @Column(name = "codes_cnt", nullable = true, columnDefinition = "BIGINT UNSIGNED") + private Long codesCnt = 0L; + + @Builder.Default + @Column(name = "projects_cnt", nullable = true, columnDefinition = "BIGINT UNSIGNED") + private Long projectsCnt = 0L; + + @Column(name = "roles") + @ElementCollection(fetch = FetchType.EAGER) + @Builder.Default + private List roles = new ArrayList<>(); + + public void projectsCntUp(){ + if(this.projectsCnt == null) + this.projectsCnt = 0L; + this.projectsCnt += 1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Users users = (Users) o; + return usersId.equals(users.usersId) && Objects.equals(email, users.email) && name.equals(users.name); + } + + @Override + public int hashCode() { + return Objects.hash(usersId, email, name); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/enums/Role.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/enums/Role.java new file mode 100644 index 00000000..c0bae0f8 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/enums/Role.java @@ -0,0 +1,6 @@ +package com.cody.roughcode.user.enums; + +public enum Role { + ROLE_USER, + ROLE_ADMIN +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/repository/UsersRepository.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/repository/UsersRepository.java new file mode 100644 index 00000000..69010248 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/repository/UsersRepository.java @@ -0,0 +1,16 @@ +package com.cody.roughcode.user.repository; + +import com.cody.roughcode.user.entity.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UsersRepository extends JpaRepository { + Optional findByEmail(String email); + + boolean existsByName(String name); + + Optional findByName(String name); + + Users findByUsersId(Long id); +} \ No newline at end of file diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/service/UsersService.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/service/UsersService.java new file mode 100644 index 00000000..46bca0b5 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/service/UsersService.java @@ -0,0 +1,17 @@ +package com.cody.roughcode.user.service; + +import com.cody.roughcode.user.dto.res.UserResp; +import com.cody.roughcode.user.entity.Users; + +public interface UsersService { + + UserResp selectOneUser(Long userId); + + default UserResp toDto(Users user) { + return UserResp.builder() + .name(user.getName()) + .email(user.getEmail()) + .build(); + } + +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/user/service/UsersServiceImpl.java b/back-end/roughcode/src/main/java/com/cody/roughcode/user/service/UsersServiceImpl.java new file mode 100644 index 00000000..17e321b9 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/user/service/UsersServiceImpl.java @@ -0,0 +1,18 @@ +package com.cody.roughcode.user.service; + +import com.cody.roughcode.user.dto.res.UserResp; +import com.cody.roughcode.user.repository.UsersRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UsersServiceImpl implements UsersService{ + + private final UsersRepository usersRepository; + + @Override + public UserResp selectOneUser(Long userId) { + return toDto(usersRepository.findById(userId).orElseThrow()); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/util/BaseTimeEntity.java b/back-end/roughcode/src/main/java/com/cody/roughcode/util/BaseTimeEntity.java new file mode 100644 index 00000000..96395e18 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/util/BaseTimeEntity.java @@ -0,0 +1,24 @@ +package com.cody.roughcode.util; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +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; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + @CreatedDate + @Column(name="created_date") + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(name="modified_date") + private LocalDateTime modifiedDate; +} \ No newline at end of file diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/util/CookieUtil.java b/back-end/roughcode/src/main/java/com/cody/roughcode/util/CookieUtil.java new file mode 100644 index 00000000..cccabf74 --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/util/CookieUtil.java @@ -0,0 +1,56 @@ +package com.cody.roughcode.util; + +import org.springframework.util.SerializationUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Base64; +import java.util.Optional; + +public class CookieUtil { + + public static Optional getCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return Optional.of(cookie); + } + } + } + return Optional.empty(); + } + + public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie: cookies) { + if (cookie.getName().equals(name)) { + cookie.setValue(""); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + } + } + + public static String serialize(Object object) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(object)); + } + + public static T deserialize(Cookie cookie, Class cls) { + return cls.cast(SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(cookie.getValue()))); + } +} diff --git a/back-end/roughcode/src/main/java/com/cody/roughcode/util/Response.java b/back-end/roughcode/src/main/java/com/cody/roughcode/util/Response.java new file mode 100644 index 00000000..03b59ffd --- /dev/null +++ b/back-end/roughcode/src/main/java/com/cody/roughcode/util/Response.java @@ -0,0 +1,67 @@ +package com.cody.roughcode.util; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * How to use + * 1. Make Controller's return type 'ResponseEntity' + * 2. Use Response.makeResponse(HttpStatus, message, result) + */ + +public class Response { + @Getter + @Builder + private static class Body { + private String message; + private Integer count; + private Object result; + } + + public static ResponseEntity makeResponse(HttpStatus httpStatus, String message, int count, Object result) { + Body body = Body.builder() + .message(message) + .count(count) + .result(result) + .build(); + + return new ResponseEntity<>(body, httpStatus); + } + + public static ResponseEntity makeResponse(HttpStatus httpStatus, String message) { + return makeResponse(httpStatus, message, 0, null); + } + + + // 200 + public static ResponseEntity ok(String message) { + return makeResponse(HttpStatus.OK, message, 0, null); + } + + // 201 + public static ResponseEntity created(String message) { + return makeResponse(HttpStatus.CREATED, message, 0, null); + } + + // 400 + public static ResponseEntity badRequest(String message) { + return makeResponse(HttpStatus.BAD_REQUEST, message, 0, null); + } + + // 401 + public static ResponseEntity noContent(String message) { + return makeResponse(HttpStatus.NO_CONTENT, message, 0, null); + } + + // 404 + public static ResponseEntity notFound(String message) { + return makeResponse(HttpStatus.NOT_FOUND, message, 0, null); + } + + // 500 + public static ResponseEntity serverError(String message) { + return makeResponse(HttpStatus.INTERNAL_SERVER_ERROR, message, 0, null); + } +} diff --git a/back-end/roughcode/src/test/java/com/cody/roughcode/RoughcodeApplicationTests.java b/back-end/roughcode/src/test/java/com/cody/roughcode/RoughcodeApplicationTests.java new file mode 100644 index 00000000..87eb0627 --- /dev/null +++ b/back-end/roughcode/src/test/java/com/cody/roughcode/RoughcodeApplicationTests.java @@ -0,0 +1,13 @@ +package com.cody.roughcode; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RoughcodeApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/back-end/roughcode/src/test/java/com/cody/roughcode/project/controller/ProjectControllerTest.java b/back-end/roughcode/src/test/java/com/cody/roughcode/project/controller/ProjectControllerTest.java new file mode 100644 index 00000000..b4f62904 --- /dev/null +++ b/back-end/roughcode/src/test/java/com/cody/roughcode/project/controller/ProjectControllerTest.java @@ -0,0 +1,276 @@ +package com.cody.roughcode.project.controller; + +import com.cody.roughcode.project.dto.req.ProjectReq; +import com.cody.roughcode.project.service.ProjectsServiceImpl; +import com.cody.roughcode.security.auth.JwtProperties; +import com.cody.roughcode.security.auth.JwtTokenProvider; +import com.cody.roughcode.user.entity.Users; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockPart; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.Cookie; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +import static com.cody.roughcode.user.enums.Role.ROLE_USER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) // @WebMVCTest를 이용할 수도 있지만 속도가 느리다 +public class ProjectControllerTest { + static { + System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true"); + } + + @InjectMocks + private ProjectsController target; + + private MockMvc mockMvc; + private Gson gson; + + @BeforeEach // 각각의 테스트가 실행되기 전에 초기화함 + public void init() { + gson = new Gson(); + mockMvc = MockMvcBuilders.standaloneSetup(target) + .build(); + } + + final Users users = Users.builder() + .usersId(1L) + .email("kosy1782@gmail.com") + .name("고수") + .roles(List.of(String.valueOf(ROLE_USER))) + .build(); + + final String accessToken = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzc2FmeTEyM0BnbWFpbC5jb20iLCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNjc0NzEyMDg2fQ.fMjhTvyLoCBzAXZ4gtJCAMS98j9DNsC7w2utcB-Uho"; + + final ProjectReq req = ProjectReq.builder() + .projectId((long) -1) + .title("title") + .url("https://www.google.com") + .introduction("introduction") + .selectedTagsId(List.of(1L)) + .content("content") + .notice("notice") + .build(); + + private static MockMultipartFile getThumbnail() throws IOException { + File imageFile = new File("src/test/java/com/cody/roughcode/resources/image/A306_ERD (2).png"); + byte[] imageBytes = Files.readAllBytes(imageFile.toPath()); + MockMultipartFile thumbnail = new MockMultipartFile( + "thumbnail", + "A306_ERD (2).png", + MediaType.IMAGE_PNG_VALUE, + imageBytes + ); + return thumbnail; + } + + @Mock + private ProjectsServiceImpl projectsService; + @Mock + private JwtTokenProvider jwtTokenProvider; + + + @DisplayName("프로젝트 정보 수정 성공") + @Test + public void updateProjectSucceed() throws Exception { + // given + final String url = "/api/v1/project/content"; + + // ProjectService updateProject 대한 stub필요 + doReturn(1).when(projectsService) + .updateProject(any(ProjectReq.class), any(Long.class)); + doReturn(1L).when(jwtTokenProvider).getId(any(String.class)); + + // when + final ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.put(url) + .cookie(new Cookie(JwtProperties.ACCESS_TOKEN, accessToken)) + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(req)) + ); + + // then + // HTTP Status가 OK인지 확인 + MvcResult mvcResult = resultActions.andExpect(status().isOk()).andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + String message = jsonObject.get("message").getAsString(); + assertThat(message).isEqualTo("프로젝트 정보 수정 성공"); + } + + @DisplayName("프로젝트 정보 수정 실패") + @Test + public void updateProjectFail() throws Exception { + // given + final String url = "/api/v1/project/content"; + + // ProjectService updateProject 대한 stub필요 + doReturn(0).when(projectsService) + .updateProject(any(ProjectReq.class), any(Long.class)); + doReturn(1L).when(jwtTokenProvider).getId(any(String.class)); + + // when + final ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.put(url) + .cookie(new Cookie(JwtProperties.ACCESS_TOKEN, accessToken)) + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(req)) + ); + + // then + // HTTP Status가 OK인지 확인 + MvcResult mvcResult = resultActions.andExpect(status().isNotFound()).andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + String message = jsonObject.get("message").getAsString(); + assertThat(message).isEqualTo("프로젝트 정보 수정 실패"); + } + + @DisplayName("프로젝트 썸네일 등록 성공") + @Test + public void updateProjectThumbnailSucceed() throws Exception { + // given + final String url = "/api/v1/project/thumbnail"; + final Long projectId = 1L; + MockMultipartFile thumbnail = getThumbnail(); + + // ProjectsService updateProjectThumbnail 대한 stub 필요 + doReturn(1).when(projectsService).updateProjectThumbnail(any(MultipartFile.class), any(Long.class), any(Long.class)); + + // when + final ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.multipart(url) + .file(thumbnail) + .param("projectId", String.valueOf(projectId)) + .cookie(new Cookie(JwtProperties.ACCESS_TOKEN, accessToken)) + ); + + // then + // HTTP Status가 OK인지 확인 + MvcResult mvcResult = resultActions.andExpect(status().isOk()).andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + String message = jsonObject.get("message").getAsString(); + assertThat(message).isEqualTo("프로젝트 썸네일 등록 성공"); + } + + @DisplayName("프로젝트 썸네일 등록 실패") + @Test + public void updateProjectThumbnailFail() throws Exception { + // given + final String url = "/api/v1/project/thumbnail"; + final Long projectId = 1L; + MockMultipartFile thumbnail = getThumbnail(); + + // ProjectsService updateProjectThumbnail 대한 stub 필요 + doReturn(-1).when(projectsService).updateProjectThumbnail(any(MultipartFile.class), any(Long.class), any(Long.class)); + + // when + final ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.multipart(url) + .file(thumbnail) + .param("projectId", String.valueOf(projectId)) + .cookie(new Cookie(JwtProperties.ACCESS_TOKEN, accessToken)) + ); + + // then + // HTTP Status가 OK인지 확인 + MvcResult mvcResult = resultActions.andExpect(status().isOk()).andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + String message = jsonObject.get("message").getAsString(); + assertThat(message).isEqualTo("프로젝트 썸네일 등록 성공"); + } + + @DisplayName("프로젝트 정보 등록 성공") + @Test + public void insertProjectSucceed() throws Exception { + // given + final String url = "/api/v1/project/content"; + + // ProjectService insertProject 대한 stub필요 + doReturn(1L).when(projectsService) + .insertProject(any(ProjectReq.class), any(Long.class)); + doReturn(1L).when(jwtTokenProvider).getId(any(String.class)); + + // when + final ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.post(url) + .cookie(new Cookie(JwtProperties.ACCESS_TOKEN, accessToken)) + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(req)) + ); + + // then + // HTTP Status가 OK인지 확인 + MvcResult mvcResult = resultActions.andExpect(status().isOk()).andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + String message = jsonObject.get("message").getAsString(); + assertThat(message).isEqualTo("프로젝트 정보 등록 성공"); + } + + @DisplayName("프로젝트 정보 등록 실패") + @Test + public void insertProjectFail() throws Exception { + // given + final String url = "/api/v1/project/content"; + + // ProjectService insertProject 대한 stub필요 + doReturn(-1L).when(projectsService) + .insertProject(any(ProjectReq.class), any(Long.class)); + doReturn(1L).when(jwtTokenProvider).getId(any(String.class)); + + // when + final ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.post(url) + .cookie(new Cookie(JwtProperties.ACCESS_TOKEN, accessToken)) + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(req)) + ); + + // then + // HTTP Status가 OK인지 확인 + MvcResult mvcResult = resultActions.andExpect(status().isNotFound()).andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + String message = jsonObject.get("message").getAsString(); + assertThat(message).isEqualTo("프로젝트 정보 등록 실패"); + } + +} diff --git a/back-end/roughcode/src/test/java/com/cody/roughcode/project/repository/ProjectRepositoryTest.java b/back-end/roughcode/src/test/java/com/cody/roughcode/project/repository/ProjectRepositoryTest.java new file mode 100644 index 00000000..a9ee7e8f --- /dev/null +++ b/back-end/roughcode/src/test/java/com/cody/roughcode/project/repository/ProjectRepositoryTest.java @@ -0,0 +1,128 @@ +package com.cody.roughcode.project.repository; + +import com.cody.roughcode.project.entity.Projects; +import com.cody.roughcode.project.entity.ProjectsInfo; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.user.repository.UsersRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.cody.roughcode.user.enums.Role.ROLE_USER; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest // 기본적으로 인메모리 데티어베이스인 H2 기반으로 테스트용 데이터베이스를 구축, 테스트가 끝나면 트랜잭션 롤백 +// 각각의 테스트 메서드가 실행될 때마다 Spring 컨텍스트를 제거하고 데이터베이스를 초기화합니다. 이렇게 하면 테스트 간에 독립성을 유지하면서 테스트를 실행할 수 있습니다. +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class ProjectRepositoryTest { + + final Users users = Users.builder() + .usersId(1L) + .email("kosy1782@gmail.com") + .name("고수") + .roles(List.of(String.valueOf(ROLE_USER))) + .build(); + + @Autowired + private ProjectsRepository projectRepository; + @Autowired + private ProjectsInfoRepository projectInfoRepository; + @Autowired + private UsersRepository usersRepository; + + @DisplayName("프로젝트 Num 가져오기") + @Test + void getProjectNum(){ + // given + usersRepository.save(users); + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("image url") + .introduction("intro") + .title("title") + .projectWriter(users) + .build(); + projectRepository.save(project); + + // when + Projects savedProjects = projectRepository.findByProjectsId(1L); + + // then + assertThat(project.getNum()).isEqualTo(savedProjects.getNum()); + } + + @DisplayName("프로젝트 Num과 User에 해당하는 Max Version 가져오기") + @Test + void getMaxProjectVersion(){ + // given + usersRepository.save(users); + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("image url") + .introduction("intro") + .title("title") + .projectWriter(users) + .build(); + Projects project2 = Projects.builder() + .projectsId(2L) + .num(1L) + .version(2) + .img("image url") + .introduction("intro") + .title("title") + .projectWriter(users) + .build(); + projectRepository.save(project); + projectRepository.save(project2); + + // when + Projects original = projectRepository.findLatestProject(1L, 1L); + + // then + assertThat(original).isEqualTo(project2); + } + + @DisplayName("프로젝트 등록") + @Test + void insertProject(){ + // given + usersRepository.save(users); + ProjectsInfo info = ProjectsInfo.builder() + .url("url") + .notice("notice") + .build(); + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("image url") + .introduction("intro") + .title("title") + .projectWriter(users) + .build(); + + // when + Projects savedProject = projectRepository.save(project); + info.setProjects(savedProject); + ProjectsInfo savedProjectInfo = projectInfoRepository.save(info); + + // then + assertThat(savedProject.getImg()).isEqualTo(project.getImg()); + assertThat(savedProject.getIntroduction()).isEqualTo(project.getIntroduction()); + assertThat(savedProject.getTitle()).isEqualTo(project.getTitle()); + + ProjectsInfo getInfo = projectInfoRepository.findByProjects(savedProject); + assertThat(getInfo.getUrl()).isEqualTo(info.getUrl()); + assertThat(getInfo.getContent()).isEqualTo(info.getContent()); + assertThat(getInfo.getNotice()).isEqualTo(info.getNotice()); + } +} diff --git a/back-end/roughcode/src/test/java/com/cody/roughcode/project/service/ProjectServiceTest.java b/back-end/roughcode/src/test/java/com/cody/roughcode/project/service/ProjectServiceTest.java new file mode 100644 index 00000000..e86c5a24 --- /dev/null +++ b/back-end/roughcode/src/test/java/com/cody/roughcode/project/service/ProjectServiceTest.java @@ -0,0 +1,582 @@ +package com.cody.roughcode.project.service; + +import com.cody.roughcode.code.repository.CodesRepostiory; +import com.cody.roughcode.exception.DeletionFailException; +import com.cody.roughcode.exception.NotMatchException; +import com.cody.roughcode.exception.NotNewestVersionException; +import com.cody.roughcode.exception.UpdateFailedException; +import com.cody.roughcode.project.dto.req.ProjectReq; +import com.cody.roughcode.project.entity.*; +import com.cody.roughcode.project.repository.*; +import com.cody.roughcode.user.entity.Users; +import com.cody.roughcode.user.repository.UsersRepository; +import org.aspectj.weaver.ast.Not; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import static com.cody.roughcode.user.enums.Role.ROLE_USER; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; + +@ExtendWith(MockitoExtension.class) // 가짜 객체 주입을 사용 +public class ProjectServiceTest { + static { + System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true"); + } + + @InjectMocks + private ProjectsServiceImpl projectsService; + + @Mock + private ProjectsRepository projectsRepository; + @Mock + private ProjectsInfoRepository projectsInfoRepository; + @Mock + private UsersRepository usersRepository; + @Mock + private CodesRepostiory codesRepostiory; + @Mock + private ProjectTagsRepository projectTagsRepository; + @Mock + private ProjectSelectedTagsRepository projectSelectedTagsRepository; + @Mock + private S3FileServiceImpl s3FileService; + @Mock + private FeedbacksRepository feedbacksRepository; + @Mock + private SelectedFeedbacksRepository selectedFeedbacksRepository; + + final Users users = Users.builder() + .usersId(1L) + .email("kosy1782@gmail.com") + .name("고수") + .roles(List.of(String.valueOf(ROLE_USER))) + .build(); + + final Users users2 = Users.builder() + .usersId(2L) + .email("kosy1782@gmail.com") + .name("고수") + .roles(List.of(String.valueOf(ROLE_USER))) + .build(); + + + final Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("https://roughcode.s3.ap-northeast-2.amazonaws.com/project/7_1") + .introduction("intro") + .title("title") + .projectWriter(users) + .projectsCodes(new ArrayList<>()) + .build(); + + private static MockMultipartFile getThumbnail() throws IOException { + File imageFile = new File("src/test/java/com/cody/roughcode/resources/image/A306_ERD (2).png"); + byte[] imageBytes = Files.readAllBytes(imageFile.toPath()); + MockMultipartFile thumbnail = new MockMultipartFile( + "thumbnail", + "A306_ERD (2).png", + MediaType.IMAGE_PNG_VALUE, + imageBytes + ); + return thumbnail; + } + + private List tagsInit() { + List tagsList = new ArrayList<>(); + for (long i = 1L; i <= 3L; i++) { + tagsList.add(ProjectTags.builder() + .tagsId(i) + .name("tag1") + .build()); + } + + return tagsList; + } + + private List feedbacksInit(Projects project) { + List feedbacksList = new ArrayList<>(); + for (long i = 1L; i <= 3L; i++) { + feedbacksList.add(Feedbacks.builder() + .projects(project) + .feedbacksId(i) + .content("content") + .users(null) + .build()); + } + + return feedbacksList; + } + + @DisplayName("프로젝트 수정 성공") + @Test + void updateProjectSucceed() throws Exception { + // given + List tagsList = tagsInit(); + Projects project = Projects.builder() + .num(1L) + .version(1) + .img("imgUrl") + .introduction("introduction") + .title("title") + .projectWriter(users) + .projectsCodes(null) + .likeCnt(1) + .selectedTags(new ArrayList<>()) + .build(); + ProjectsInfo info = ProjectsInfo.builder() + .url("www.google.com") + .notice("notice") + .build(); + List feedbacksList = feedbacksInit(project); + + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title2") + .url("https://www.google.com") + .introduction("introduction2") + .selectedTagsId(List.of(1L, 2L)) + .content("content2") + .notice("notice2") + .selectedFeedbacksId(List.of(1L, 2L)) + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(project).when(projectsRepository).findByProjectsId(any(Long.class)); + doReturn(project).when(projectsRepository).findLatestProject(any(Long.class), any(Long.class)); + doReturn(info).when(projectsInfoRepository).findByProjects(any(Projects.class)); + doReturn(tagsList.get(0)).when(projectTagsRepository).findByTagsId(any(Long.class)); + doReturn(feedbacksList.get(0)).when(feedbacksRepository).findFeedbacksByFeedbacksId(any(Long.class)); + + // when + int success = projectsService.updateProject(req, 1L); + + // then + assertThat(success).isEqualTo(1); + } + + // 일치하는 프로젝트 없음 + @DisplayName("프로젝트 수정 실패 - 일치하는 프로젝트 없음") + @Test + void updateProjectFailNoProject() throws Exception { + // given + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title2") + .url("https://www.google.com") + .introduction("introduction2") + .selectedTagsId(List.of(1L, 2L)) + .content("content2") + .notice("notice2") + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(null).when(projectsRepository).findByProjectsId(any(Long.class)); + + // when & then + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.updateProject(req, 1L) + ); + + assertEquals("일치하는 프로젝트가 존재하지 않습니다", exception.getMessage()); + } + + // 최신 버전의 프로젝트가 아님 + @DisplayName("프로젝트 수정 실패 - 최신 버전의 프로젝트가 아님") + @Test + void updateProjectFailNotNewest() throws Exception { + // given + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("imgUrl") + .introduction("introduction") + .title("title") + .projectWriter(users) + .projectsCodes(null) + .likeCnt(1) + .selectedTags(new ArrayList<>()) + .build(); + Projects project2 = Projects.builder() + .projectsId(2L) + .num(2L) + .version(1) + .img("imgUrl") + .introduction("introduction") + .title("title") + .projectWriter(users) + .projectsCodes(null) + .likeCnt(1) + .selectedTags(new ArrayList<>()) + .build(); + + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title2") + .url("https://www.google.com") + .introduction("introduction2") + .selectedTagsId(List.of(1L, 2L)) + .content("content2") + .notice("notice2") + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(project).when(projectsRepository).findByProjectsId(any(Long.class)); + doReturn(project2).when(projectsRepository).findLatestProject(any(Long.class), any(Long.class)); + + // when & then + NotNewestVersionException exception = assertThrows( + NotNewestVersionException.class, () -> projectsService.updateProject(req, 1L) + ); + + assertEquals("최신 버전이 아닙니다", exception.getMessage()); + } + + // 일치하는 프로젝트 정보가 없음 + @DisplayName("프로젝트 수정 실패 - 일치하는 프로젝트 정보가 없음") + @Test + void updateProjectFailNoProjectInfo() throws Exception { + // given + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("imgUrl") + .introduction("introduction") + .title("title") + .projectWriter(users) + .projectsCodes(null) + .likeCnt(1) + .selectedTags(new ArrayList<>()) + .build(); + + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title2") + .url("https://www.google.com") + .introduction("introduction2") + .selectedTagsId(List.of(1L, 2L)) + .content("content2") + .notice("notice2") + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(project).when(projectsRepository).findByProjectsId(any(Long.class)); + doReturn(project).when(projectsRepository).findLatestProject(any(Long.class), any(Long.class)); + doReturn(null).when(projectsInfoRepository).findByProjects(any(Projects.class)); + + // when & then + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.updateProject(req, 1L) + ); + + assertEquals("일치하는 프로젝트가 존재하지 않습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 썸네일 등록 성공") + @Test + void updateProjectThumbnailSucceed() throws IOException { + // given + MockMultipartFile thumbnail = getThumbnail(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(project).when(projectsRepository).findByProjectsId(any(Long.class)); + doReturn(project).when(projectsRepository).findLatestProject(any(Long.class), any(Long.class)); + doReturn("imageUrl").when(s3FileService).upload(any(MultipartFile.class), any(String.class), any(String.class)); + + // when + int success = projectsService.updateProjectThumbnail(thumbnail, 1L, 1L); + + // then + assertThat(success).isEqualTo(1L); + } + + @DisplayName("프로젝트 썸네일 등록 실패 - 존재하지 않는 유저 아이디") + @Test + void updateProjectThumbnailFailNoUser() throws IOException { + // given + MockMultipartFile thumbnail = getThumbnail(); + doReturn(null).when(usersRepository).findByUsersId(any(Long.class)); + + // when & then + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.updateProjectThumbnail(thumbnail, 1L, 1L) + ); + + assertEquals("일치하는 유저가 존재하지 않습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 썸네일 등록 실패 - 썸네일이 등록되어있지 않음") + @Test + void updateProjectThumbnailFailNoThumbnail() throws IOException { + // given + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + + // when & then + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.updateProjectThumbnail(null, 1L, 1L) + ); + + assertEquals("썸네일이 등록되어있지 않습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 썸네일 등록 실패 - 일치하는 프로젝트가 없음") + @Test + void updateProjectThumbnailFailNoProject() throws IOException { + MockMultipartFile thumbnail = getThumbnail(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(null).when(projectsRepository).findByProjectsId(any(Long.class)); + + // when & then + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.updateProjectThumbnail(thumbnail, 1L, 1L) + ); + + assertEquals("일치하는 프로젝트가 없습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 썸네일 등록 실패 - 등록하려는 유저와 프로젝트의 유저가 일치하지 않음") + @Test + void updateProjectThumbnailFailUserDiffer() throws IOException { + + MockMultipartFile thumbnail = getThumbnail(); + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("https://roughcode.s3.ap-northeast-2.amazonaws.com/project/7_1") + .introduction("intro") + .title("title") + .projectWriter(users) + .projectsCodes(new ArrayList<>()) + .build(); + + doReturn(users2).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(project).when(projectsRepository).findByProjectsId(any(Long.class)); + + // when & then + NotMatchException exception = assertThrows( + NotMatchException.class, () -> projectsService.updateProjectThumbnail(thumbnail, 1L, 1L) + ); + + assertEquals("접근 권한이 없습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 등록 성공 - 새 프로젝트") + @Test + void insertProjectSucceed() { + // given + List tagsList = tagsInit(); + ProjectReq req = ProjectReq.builder() + .projectId((long) -1) + .title("title") + .url("https://www.google.com") + .introduction("introduction") + .selectedTagsId(List.of(1L)) + .content("content") + .notice("notice") + .build(); + + Projects project = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("https://roughcode.s3.ap-northeast-2.amazonaws.com/project/7_1") + .introduction(req.getIntroduction()) + .title(req.getTitle()) + .projectWriter(users) + .projectsCodes(new ArrayList<>()) + .build(); + ProjectsInfo info = ProjectsInfo.builder() + .url(req.getUrl()) + .notice(req.getNotice()) + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(users).when(usersRepository).save(any(Users.class)); + doReturn(project).when(projectsRepository).save(any(Projects.class)); + doReturn(tagsList.get(0)).when(projectTagsRepository).findByTagsId(any(Long.class)); + doReturn(ProjectSelectedTags.builder() + .tags(tagsList.get(0)) + .projects(project) + .build()) + .when(projectSelectedTagsRepository) + .save(any(ProjectSelectedTags.class)); + doReturn(tagsList.get(0)).when(projectTagsRepository).save(any(ProjectTags.class)); + doReturn(info).when(projectsInfoRepository).save(any(ProjectsInfo.class)); + + // when + Long success = projectsService.insertProject(req, 1L); + + // then + assertThat(success).isEqualTo(1L); + } + + @DisplayName("프로젝트 등록 성공 - 기존 프로젝트 업데이트") + @Test + void insertProjectSucceedVersionUp() { + // given + List tagsList = tagsInit(); + ProjectReq req = ProjectReq.builder() + .projectId((long) 1) + .title("title") + .url("https://www.google.com") + .introduction("introduction") + .selectedTagsId(List.of(1L)) + .content("content") + .notice("notice") + .build(); + + Projects project = Projects.builder() + .projectsId(2L) + .num(1L) + .version(2) + .img("https://roughcode.s3.ap-northeast-2.amazonaws.com/project/7_1") + .introduction(req.getIntroduction()) + .title(req.getTitle()) + .projectWriter(users) + .projectsCodes(new ArrayList<>()) + .build(); + ProjectsInfo info = ProjectsInfo.builder() + .url(req.getUrl()) + .notice(req.getNotice()) + .build(); + + Projects original = Projects.builder() + .num(1L) + .version(1) + .img("https://roughcode.s3.ap-northeast-2.amazonaws.com/project/7_1") + .introduction(req.getIntroduction()) + .title(req.getTitle()) + .projectWriter(users) + .projectsCodes(new ArrayList<>()) + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(original).when(projectsRepository).findByProjectsId(any(Long.class)); + doReturn(original).when(projectsRepository).findLatestProject(any(Long.class), any(Long.class)); + doReturn(project).when(projectsRepository).save(any(Projects.class)); + doReturn(tagsList.get(0)).when(projectTagsRepository).findByTagsId(any(Long.class)); + doReturn(ProjectSelectedTags.builder() + .tags(tagsList.get(0)) + .projects(project) + .build()) + .when(projectSelectedTagsRepository) + .save(any(ProjectSelectedTags.class)); + doReturn(tagsList.get(0)).when(projectTagsRepository).save(any(ProjectTags.class)); + doReturn(info).when(projectsInfoRepository).save(any(ProjectsInfo.class)); + + // when + Long success = projectsService.insertProject(req, 1L); + + // then + assertThat(success).isEqualTo(2); + } + + @DisplayName("프로젝트 등록 실패 - 존재하지 않는 유저 아이디") + @Test + void insertProjectFailNoUser() { + // given + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title") + .url("https://www.google.com") + .introduction("introduction") + .selectedTagsId(List.of(1L)) + .content("content") + .notice("notice") + .build(); + + // when & then + doReturn(null).when(usersRepository).findByUsersId(any(Long.class)); + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.insertProject(req, 1L) + ); + + assertEquals("일치하는 유저가 존재하지 않습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 등록 실패 - 존재하지 않는 project id") + @Test + void insertProjectFailNoProject() { + // given + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title") + .url("https://www.google.com") + .introduction("introduction") + .selectedTagsId(List.of(1L)) + .content("content") + .notice("notice") + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(null).when(projectsRepository).findByProjectsId(any(Long.class)); + + // when & then + NullPointerException exception = assertThrows( + NullPointerException.class, () -> projectsService.insertProject(req, 1L) + ); + + assertEquals("일치하는 프로젝트가 존재하지 않습니다", exception.getMessage()); + } + + @DisplayName("프로젝트 등록 실패 - project 작성한 user랑 version up 하려는 user가 다름") + @Test + void insertProjectFailUserDiffer() { + // given + ProjectReq req = ProjectReq.builder() + .projectId(1L) + .title("title") + .url("https://www.google.com") + .introduction("introduction") + .selectedTagsId(List.of(1L)) + .content("content") + .notice("notice") + .build(); + + Projects original = Projects.builder() + .projectsId(1L) + .num(1L) + .version(1) + .img("https://roughcode.s3.ap-northeast-2.amazonaws.com/project/7_1") + .introduction(req.getIntroduction()) + .title(req.getTitle()) + .projectWriter(users2) + .projectsCodes(new ArrayList<>()) + .build(); + + doReturn(users).when(usersRepository).findByUsersId(any(Long.class)); + doReturn(original).when(projectsRepository).findByProjectsId(any(Long.class)); + doReturn(original).when(projectsRepository).findLatestProject(any(Long.class), any(Long.class)); + + // when & then + NotMatchException exception = assertThrows( + NotMatchException.class, () -> projectsService.insertProject(req, 1L) + ); + + assertEquals("접근 권한이 없습니다", exception.getMessage()); + } + +} diff --git a/back-end/roughcode/src/test/java/com/cody/roughcode/resources/image/A306_ERD (2).png b/back-end/roughcode/src/test/java/com/cody/roughcode/resources/image/A306_ERD (2).png new file mode 100644 index 00000000..02a95f6b Binary files /dev/null and b/back-end/roughcode/src/test/java/com/cody/roughcode/resources/image/A306_ERD (2).png differ