diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/aws.xml b/.idea/aws.xml new file mode 100644 index 00000000..b63b642c --- /dev/null +++ b/.idea/aws.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..cee3df1d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..e1895728 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..fdc392fe --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..d03ab384 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..7f32e8df --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/008main_project.main.iml b/.idea/modules/008main_project.main.iml new file mode 100644 index 00000000..5e995830 --- /dev/null +++ b/.idea/modules/008main_project.main.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/008main_project.test.iml b/.idea/modules/008main_project.test.iml new file mode 100644 index 00000000..466fc6ee --- /dev/null +++ b/.idea/modules/008main_project.test.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/seb45_main_008.iml b/.idea/seb45_main_008.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/seb45_main_008.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 00000000..2b63946d --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index e69de29b..723bf2a5 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ + + +## 45연대 8특임대 + +| 양재용
(BE 팀장) | 이강욱
(BE) | 이준석
(BE) | 조현덕
(BE) | 김병현
(FE 부팀장) | 김영학
(FE) | 신중원
(FE) | +|----------------------------------------------------------------------------------------------------------------------------|-----------|--------------------------------------------|--------------------------------------------|----------------------------------------------|----------------------------------------------|---------------| +| [@yjy8501](https://github.com/yjy8501) | [@rkddnr05](https://github.com/rkddnr05) | [@IncheonLee](https://github.com/IncheonLee) | [@ChoHD](https://github.com/ChoHD) | [@sirloinbh](https://github.com/shimdokite) | [@novice1993](https://github.com/novice1993) |[@sinjw ](https://github.com/sinjw)| + +### 배포 주소 +http://seb008stockholm.s3-website.ap-northeast-2.amazonaws.com/ + +### Tools + + [![stackticon](https://firebasestorage.googleapis.com/v0/b/stackticon-81399.appspot.com/o/images%2F1695089957938?alt=media&token=b049edc2-83f9-4d29-b205-9f5f9b3492f1)](https://github.com/msdio/stackticon) + +### 기술 스택 + +### BE + +[![stackticon](https://firebasestorage.googleapis.com/v0/b/stackticon-81399.appspot.com/o/images%2F1695133984245?alt=media&token=fef31d2b-813d-41ba-a173-eae6e8c6f19b)](https://github.com/msdio/stackticon) + +### FE + +[![stackticon](https://firebasestorage.googleapis.com/v0/b/stackticon-81399.appspot.com/o/images%2F1695134306560?alt=media&token=13e75353-3e07-4228-85a7-261c00e1b47f)](https://github.com/msdio/stackticon) + +## API 명세서 +http://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com:8080/swagger-ui/index.html + +## USER Flow +https://drive.google.com/file/d/1ej4sYQusG8g3uFPHRILq7Khd8m1evz4X/view?usp=sharing + +## 화면 정의서 +https://docs.google.com/presentation/d/1yyUBoyX8Yo7ZQxZXg2CFiZDh6W0j4_jSM8UTPLbnvy8/edit?usp=sharing + +## 요구 사항 정의서 +https://docs.google.com/spreadsheets/d/1SYxtZdxv-H4Cm4O124we4IPbjh4eNBDV/edit#gid=1436212772 + +## ERD +![화면 캡처 2023-09-20 000142](https://github.com/codestates-seb/seb45_main_008/assets/130031798/0d52cc8e-2eac-47a5-baec-1d19d98c9d08) + + +## Github Rule + +### Commit Convention + +| Message | 설명 | +| :--------: | :---------------------------------------------------- | +| `feat` | 새로운 기능 추가 | +| `fix` | 버그 수정 | +| `refactor` | 코드 리팩토링 | +| `style` | 코드 포맷팅, 세미콜론 누락, 코드 스타일 변경 등 | +| `remove` | 사용하지 않는 파일 또는 폴더 삭제 | +| `rename` | 파일 또는 폴더명 수정 | diff --git a/server/008main_project/.github/workflows/main.yml b/server/008main_project/.github/workflows/main.yml new file mode 100644 index 00000000..174154de --- /dev/null +++ b/server/008main_project/.github/workflows/main.yml @@ -0,0 +1,26 @@ +name: Java CI with Gradle + +on: + pull_request: + branches: [ dev-server ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'zulu' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - name: Docker build + run: | + docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} + docker buildx build --push --platform linux/amd64,linux/arm64 -t ${{ secrets.DOCKER_HUB_USERNAME }}/stockholm:${GITHUB_SHA::7} . diff --git a/server/008main_project/.gitignore b/server/008main_project/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/server/008main_project/.gitignore @@ -0,0 +1,37 @@ +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/ diff --git a/server/008main_project/Dockerfile b/server/008main_project/Dockerfile new file mode 100644 index 00000000..28d757b7 --- /dev/null +++ b/server/008main_project/Dockerfile @@ -0,0 +1,11 @@ +# (1) base-image +FROM openjdk:11 + +# (2) COPY에서 사용될 경로 변수 +ARG JAR_FILE=build/libs/*-SNAPSHOT.jar + +# (3) jar 빌드 파일을 도커 컨테이너로 복사 +COPY ${JAR_FILE} app.jar + +# (4) jar 파일 실행 +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/server/008main_project/build.gradle b/server/008main_project/build.gradle new file mode 100644 index 00000000..c976b374 --- /dev/null +++ b/server/008main_project/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '2.7.15' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' +} + +group = 'com.stockholm' +version = '0.0.1-SNAPSHOT' + +java { + 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-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springdoc:springdoc-openapi-ui:1.6.15' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' //버전 수정 + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' //추가 + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + // security 작업을 위한 의존성 + implementation 'org.springframework.boot:spring-boot-starter-mail:2.7.0' //mail 전송을 위한 추가 + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'com.google.code.gson:gson:2.8.8' // gson 의존성 추가 + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'software.amazon.awssdk:s3:2.17.52' // AWS S3 의존성 추가 +} + +tasks.named('test') { + useJUnitPlatform() +} + +jar { + enabled = false +} diff --git a/server/008main_project/gradle/wrapper/gradle-wrapper.jar b/server/008main_project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..033e24c4 Binary files /dev/null and b/server/008main_project/gradle/wrapper/gradle-wrapper.jar differ diff --git a/server/008main_project/gradle/wrapper/gradle-wrapper.properties b/server/008main_project/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..9f4197d5 --- /dev/null +++ b/server/008main_project/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/server/008main_project/gradlew b/server/008main_project/gradlew new file mode 100755 index 00000000..fcb6fca1 --- /dev/null +++ b/server/008main_project/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, 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/server/008main_project/gradlew.bat b/server/008main_project/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/server/008main_project/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +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/server/008main_project/settings.gradle b/server/008main_project/settings.gradle new file mode 100644 index 00000000..be65ed9f --- /dev/null +++ b/server/008main_project/settings.gradle @@ -0,0 +1 @@ +rootProject.name = '008main_project' diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/Application.java b/server/008main_project/src/main/java/com/stockholm/main_project/Application.java new file mode 100644 index 00000000..074cc8c4 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/Application.java @@ -0,0 +1,20 @@ +package com.stockholm.main_project; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; + +import javax.annotation.PostConstruct; +import java.util.TimeZone; + +@EnableScheduling +@EnableJpaAuditing +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/Category/Controller/CategoryController.java b/server/008main_project/src/main/java/com/stockholm/main_project/Category/Controller/CategoryController.java new file mode 100644 index 00000000..0c28d0c6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/Category/Controller/CategoryController.java @@ -0,0 +1,4 @@ +//package com.stockholm.main_project.Category.Controller; +// +//public class CategoryController { +//} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/Category/Entity/CategoryEntity.java b/server/008main_project/src/main/java/com/stockholm/main_project/Category/Entity/CategoryEntity.java new file mode 100644 index 00000000..45363a51 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/Category/Entity/CategoryEntity.java @@ -0,0 +1,4 @@ +//package com.stockholm.main_project.Category.Entity; +// +//public class CategoryEntity { +//} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/Category/Repository/CategoryRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/Category/Repository/CategoryRepository.java new file mode 100644 index 00000000..d161058d --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/Category/Repository/CategoryRepository.java @@ -0,0 +1,4 @@ +//package com.stockholm.main_project.Category.Repository; +// +//public interface CategoryRepository { +//} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/advice/GlobalExceptionAdvice.java b/server/008main_project/src/main/java/com/stockholm/main_project/advice/GlobalExceptionAdvice.java new file mode 100644 index 00000000..c3d2ea28 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/advice/GlobalExceptionAdvice.java @@ -0,0 +1,48 @@ +package com.stockholm.main_project.advice; + +import com.stockholm.main_project.auth.erroesponse.ErrorResponse; +import com.stockholm.main_project.exception.BusinessLogicException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.validation.ConstraintViolationException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionAdvice { + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { + final ErrorResponse response = ErrorResponse.of(e.getBindingResult()); + + return response; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleConstraintViolationException( + ConstraintViolationException e) { + final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations()); + + return response; + } + + //추가 작업 필요 + @ExceptionHandler + public ResponseEntity handleBusinessLogicException(BusinessLogicException e) { + System.out.println(e.getExceptionCode().getStatus()); + System.out.println(e.getMessage()); + final ErrorResponse response = ErrorResponse.of(e.getExceptionCode()); + return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode().getStatus())); + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/audit/Auditable.java b/server/008main_project/src/main/java/com/stockholm/main_project/audit/Auditable.java new file mode 100644 index 00000000..812dfa7b --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/audit/Auditable.java @@ -0,0 +1,24 @@ +package com.stockholm.main_project.audit; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class Auditable { + @CreatedDate + @Column(updatable = false) + LocalDateTime createdAt; + + @LastModifiedDate + @Column + LocalDateTime modifiedAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/attribute/OAuthAttributes.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/attribute/OAuthAttributes.java new file mode 100644 index 00000000..1de5a512 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/attribute/OAuthAttributes.java @@ -0,0 +1,55 @@ +package com.stockholm.main_project.auth.attribute; + +import com.stockholm.main_project.member.entity.Member; +import lombok.Builder; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; + +@Getter +@Builder +public class OAuthAttributes { + private Map attributes; + private String nameAttributeKey; + private String name; + private String email; + public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map attributes) { + + if ("kakao".equals(registrationId)) { + return ofKakao("id", attributes);} + + return ofGoogle(userNameAttributeName, attributes); + } + + + private static OAuthAttributes ofKakao(String userNameAttributeName, Map attributes) { + Map response = (Map) attributes.get("kakao_account"); + Map account = (Map) response.get("profile"); + + + return OAuthAttributes.builder() + .name((String) account.get("nickname")) + .email((String) response.get("email")) + .attributes(response) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + private static OAuthAttributes ofGoogle(String userNameAttributeName, Map attributes) { + return OAuthAttributes.builder() + .name((String) attributes.get("name")) + .email((String) attributes.get("email")) + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + public Member toEntity() { + return Member.builder() + .name(name) + .email(email) + .roles(Arrays.asList("USER")) + .build(); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/dto/LoginDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/dto/LoginDto.java new file mode 100644 index 00000000..fa0353f0 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/dto/LoginDto.java @@ -0,0 +1,10 @@ +package com.stockholm.main_project.auth.dto; + +import lombok.Getter; + +@Getter +public class LoginDto { + private String email; + private String password; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/dto/TokenDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/dto/TokenDto.java new file mode 100644 index 00000000..52008e93 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/dto/TokenDto.java @@ -0,0 +1,16 @@ +package com.stockholm.main_project.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class TokenDto { + + @AllArgsConstructor + @Getter + public static class Response { + private String accessToken; + private String refreshToken; + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java new file mode 100644 index 00000000..0d8d19b5 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/erroesponse/ErrorResponse.java @@ -0,0 +1,97 @@ +package com.stockholm.main_project.auth.erroesponse; + +import com.stockholm.main_project.exception.ExceptionCode; +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; + +import javax.validation.ConstraintViolation; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Getter +public class ErrorResponse { + private int status; + private String message; + private List fieldErrors; + private List violationErrors; + + private ErrorResponse(int status, String message) { + this.status = status; + this.message = message; + } + + private ErrorResponse(List fieldErrors, + List violationErrors) { + this.fieldErrors = fieldErrors; + this.violationErrors = violationErrors; + } + + public static ErrorResponse of(BindingResult bindingResult) { + return new ErrorResponse(FieldError.of(bindingResult), null); + } + + public static ErrorResponse of(Set> violations) { + return new ErrorResponse(null, ConstraintViolationError.of(violations)); + } + + public static ErrorResponse of(ExceptionCode exceptionCode) { + return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage()); + } + + public static ErrorResponse of(HttpStatus httpStatus) { + return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase()); + } + + @Getter + + public static class FieldError { + private String field; + private Object rejectedValue; + private String reason; + + private FieldError(String field, Object rejectedValue, String reason) { + this.field = field; + this.rejectedValue = rejectedValue; + this.reason = reason; + } + + public static List of(BindingResult bindingResult) { + final List fieldErrors = + bindingResult.getFieldErrors(); + return fieldErrors.stream() + .map(error -> new FieldError( + error.getField(), + error.getRejectedValue() == null ? + "" : error.getRejectedValue().toString(), + error.getDefaultMessage())) + .collect(Collectors.toList()); + } + } + + @Getter + public static class ConstraintViolationError { + private String propertyPath; + private Object rejectedValue; + private String reason; + + private ConstraintViolationError(String propertyPath, Object rejectedValue, + String reason) { + this.propertyPath = propertyPath; + this.rejectedValue = rejectedValue; + this.reason = reason; + } + + public static List of( + Set> constraintViolations) { + return constraintViolations.stream() + .map(constraintViolation -> new ConstraintViolationError( + constraintViolation.getPropertyPath().toString(), + constraintViolation.getInvalidValue().toString(), + constraintViolation.getMessage() + )).collect(Collectors.toList()); + } + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..0837423c --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,86 @@ +package com.stockholm.main_project.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.dto.LoginDto; +import com.stockholm.main_project.member.entity.Member; +import lombok.SneakyThrows; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JwtTokenizer jwtTokenizer; + + public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) { + this.authenticationManager = authenticationManager; + this.jwtTokenizer = jwtTokenizer; + } + + @SneakyThrows + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { + ObjectMapper objectMapper = new ObjectMapper(); + LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword()); + + return authenticationManager.authenticate(authenticationToken); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + Authentication authResult) throws ServletException, IOException { + Member member = (Member) authResult.getPrincipal(); + + String accessToken = delegateAccessToken(member); + String refreshToken = delegateRefreshToken(member); + + response.setHeader("Authorization", "Bearer " + accessToken); + response.setHeader("Refresh", refreshToken); + //response.setHeader("Access-Control-Expose-Headers", "Authorization"); + //response.addHeader("Access-Control-Expose-Headers", "MemberId"); + + this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult); + } + + + private String delegateAccessToken(Member member) { + Map claims = new HashMap<>(); + claims.put("memberId", member.getMemberId()); //Context에 member객체를 올려두기 위해 추가 + claims.put("email", member.getEmail()); + claims.put("roles", member.getRoles()); + + String subject = member.getEmail(); + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes()); + + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey); + + return accessToken; + } + + private String delegateRefreshToken(Member member) { + String subject = member.getEmail(); + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes()); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey); + + return refreshToken; + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java new file mode 100644 index 00000000..6a4a7886 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/filter/JwtVerificationFilter.java @@ -0,0 +1,66 @@ +package com.stockholm.main_project.auth.filter; + +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.service.MemberService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Slf4j +public class JwtVerificationFilter extends OncePerRequestFilter { + private final JwtTokenizer jwtTokenizer; + private final CustomAuthorityUtils authorityUtils; + + private final MemberService memberService; + // (2) + public JwtVerificationFilter(JwtTokenizer jwtTokenizer, + CustomAuthorityUtils authorityUtils, MemberService memberService) { + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + this.memberService = memberService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Map claims = verifyJws(request); + setAuthenticationToContext(claims); + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String authorization = request.getHeader("Authorization"); + + return authorization == null || !authorization.startsWith("Bearer"); + } + + private Map verifyJws(HttpServletRequest request) { + String jws = request.getHeader("Authorization").replace("Bearer ", ""); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + Map claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody(); + + return claims; + } + + private void setAuthenticationToContext(Map claims) { + int memberId = (int)claims.get("memberId"); + Member member = memberService.findMember(memberId); + log.info(String.valueOf(member)); + List authorities = authorityUtils.createAuthorities((List)claims.get("roles")); + Authentication authentication = new UsernamePasswordAuthenticationToken(member, null, authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java new file mode 100644 index 00000000..601b8b67 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationFailureHandler.java @@ -0,0 +1,37 @@ +package com.stockholm.main_project.auth.handler; + +import com.google.gson.Gson; +import com.stockholm.main_project.auth.erroesponse.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + log.error("# Authentication failed: {}", exception.getMessage()); + + sendErrorResponse(response); + } + + private void sendErrorResponse(HttpServletResponse response) throws IOException { + Gson gson = new Gson(); + ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class)); + } +} + + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java new file mode 100644 index 00000000..71e4e9ac --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/MemberAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package com.stockholm.main_project.auth.handler; + +import com.google.gson.Gson; +import com.stockholm.main_project.auth.erroesponse.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class MemberAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + log.info("# Authenticated successfully!"); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/OAuth2AuthenticationSuccessHandler.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 00000000..ae42c29a --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/handler/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,126 @@ +package com.stockholm.main_project.auth.handler; + +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.member.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenizer jwtTokenizer; + + private final CustomAuthorityUtils authorityUtils; + private final MemberService memberService; + + @Autowired + public OAuth2AuthenticationSuccessHandler(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils, MemberService memberService) { + + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + this.memberService = memberService; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + var oAuth2User = (OAuth2User)authentication.getPrincipal(); + + String email = String.valueOf(oAuth2User.getAttributes().get("email")); + + List authorities = authorityUtils.createRoles(email); + + // 액세스 토큰 및 리프레시 토큰 생성 + String accessToken = delegateAccessToken(email, authorities); + String refreshToken = delegateRefreshToken(email); + + // 헤더에 토큰 추가 + response.addHeader("Authorization", "Bearer " + accessToken); + response.addHeader("Refresh-Token", refreshToken); + + + redirect(request, response, email, authorities); + } + + private void redirect(HttpServletRequest request, HttpServletResponse response, String username, List authorities) throws IOException { + String accessToken = delegateAccessToken(username, authorities); + String refreshToken = delegateRefreshToken(username); + + String uri = createURI(accessToken, refreshToken).toString(); + getRedirectStrategy().sendRedirect(request, response, uri); + } + + private String delegateAccessToken(String username, List authorities) { + // 사용자의 이메일로 Member ID를 가져옴 + int memberId = memberService.findMemberIdByEmail(username); + + Map claims = new HashMap<>(); + claims.put("email", username); + claims.put("roles", authorities); + claims.put("memberId", memberId); // Member ID를 클레임에 추가 + + String subject = username; + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes()); + + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey); + + return accessToken; + } + + private String delegateRefreshToken(String username) { + + + String subject = username; + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes()); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey); + + return refreshToken; + } + + private URI createURI(String accessToken, String refreshToken) { + + MultiValueMap queryParams = new LinkedMultiValueMap<>(); + queryParams.add("access_token", accessToken); + queryParams.add("refresh_token", refreshToken); + +// return UriComponentsBuilder +// .newInstance() +// .scheme("http") +// .host("localhost") +// .port(8080) +// .queryParams(queryParams) +// .path("/receive-token.html") +// .build() +// .toUri(); +// } + return UriComponentsBuilder + .newInstance() + .scheme("http") + .host("seb008stockholm.s3-website.ap-northeast-2.amazonaws.com") +// .port(8080) + .queryParams(queryParams) + .path("/") + .build() + .toUri(); + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java new file mode 100644 index 00000000..8049e974 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/jwt/JwtTokenizer.java @@ -0,0 +1,101 @@ +package com.stockholm.main_project.auth.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + + +@Component +public class JwtTokenizer { + + @Getter + @Value("${jwt.key}") + private String secretKey; + + @Getter + @Value("${jwt.access-token-expiration-minutes}") //Access Token에 대한 만료 시간 정보 + private int accessTokenExpirationMinutes; + + @Getter + @Value("${jwt.refresh-token-expiration-minutes}") //Refresh Token에 대한 만료 시간 정보 + private int refreshTokenExpirationMinutes; + + public String encodeBase64SecretKey(String secretKey){ + + return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + public String generateAccessToken(Map claims, + String subject, + Date expiration, + String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + + public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + + public Jws getClaims(String jws, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + Jws claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + return claims; + } + + public void verifySignature(String jws, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + + } + + public Date getTokenExpiration(int expirationMinutes) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, expirationMinutes); + Date expiration = calendar.getTime(); + + return expiration; + } + + private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) { + byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); + Key key = Keys.hmacShaKeyFor(keyBytes); + + return key; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/AccountController.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/AccountController.java new file mode 100644 index 00000000..bd4fced5 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/AccountController.java @@ -0,0 +1,61 @@ +package com.stockholm.main_project.auth.mail; + +import com.stockholm.main_project.auth.mail.Dto.ConfirmPostDto; +import com.stockholm.main_project.auth.mail.Dto.SendEmailPostDto; +import com.stockholm.main_project.member.dto.MemberPatchDto; +import com.stockholm.main_project.member.dto.MemberResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RequiredArgsConstructor +@RestController +public class AccountController { + + private final EmailService emailService; + + + + @Operation(summary = "Email 발송", description = "Email 검증을 위한 코드가 POST됩니다.", tags = { "Email" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = SendEmailPostDto.class), + examples = @ExampleObject(value = "4148WEAW1"))) + @ApiResponse(responseCode = "400", description = "BAD REQUEST") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @PostMapping("/email/send") + public String emailSend(@Schema(implementation = SendEmailPostDto.class) @RequestBody SendEmailPostDto sendEmailPostDto) throws Exception { + + String confirm = emailService.sendSimpleMessage(sendEmailPostDto.getEmail()); + + return confirm; + } + + @Operation(summary = "Email 검증", description = "Email로 발송된 코드에 대한 검증을 POST됩니다.", tags = { "Email" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = ConfirmPostDto.class), + examples = @ExampleObject(value = "인증 성공"))) + @ApiResponse(responseCode = "400", description = "BAD REQUEST", + content = @Content(schema = @Schema(implementation = ConfirmPostDto.class), + examples = @ExampleObject(value = "인증 실패"))) + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @PostMapping("/email/confirm") + public ResponseEntity emailConfirm(@Schema(implementation = ConfirmPostDto.class) @RequestBody ConfirmPostDto confirmPostDto) throws Exception { + // 사용자가 입력한 코드(code)와 생성된 코드(ePw)를 비교 + String email = confirmPostDto.getEmail(); + String code = confirmPostDto.getCode(); + + if (code.equals(EmailServiceImpl.ePw)) { + return ResponseEntity.status(HttpStatus.OK).body("인증 성공"); + } else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("인증 실패"); + } + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/ConfirmPostDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/ConfirmPostDto.java new file mode 100644 index 00000000..bf78264f --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/ConfirmPostDto.java @@ -0,0 +1,22 @@ +package com.stockholm.main_project.auth.mail.Dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Getter +@Setter +public class ConfirmPostDto { + @Email + @NotBlank + @Pattern(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$", + message = "올바른 이메일 구성이 아닙니다.") + @Schema(description = "Email", defaultValue = "Test@example.com") + private String email; + @Schema(description = "승인 코드", defaultValue = "4148WEAW1") + private String code; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/SendEmailPostDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/SendEmailPostDto.java new file mode 100644 index 00000000..27b3a563 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/Dto/SendEmailPostDto.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.auth.mail.Dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Getter +@Setter +public class SendEmailPostDto { + + @Email + @NotBlank + @Pattern(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$", + message = "올바른 이메일 구성이 아닙니다.") + @Schema(description = "Email", defaultValue = "Test@example.com") + private String email; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailConfig.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailConfig.java new file mode 100644 index 00000000..accf584a --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailConfig.java @@ -0,0 +1,55 @@ +package com.stockholm.main_project.auth.mail; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +@PropertySource("classpath:stockholmMail.properties") +public class EmailConfig { + + @Value("${mail.smtp.port}") + private int port; + @Value("${mail.smtp.socketFactory.port}") + private int socketPort; + @Value("${mail.smtp.auth}") + private boolean auth; + @Value("${mail.smtp.starttls.enable}") + private boolean starttls; + @Value("${mail.smtp.starttls.required}") + private boolean startlls_required; + @Value("${mail.smtp.socketFactory.fallback}") + private boolean fallback; + @Value("${AdminMail.id}") + private String id; + @Value("${AdminMail.password}") + private String password; + + @Bean + public JavaMailSender javaMailService() { + JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); + javaMailSender.setHost("smtp.gmail.com"); + javaMailSender.setUsername(id); + javaMailSender.setPassword(password); + javaMailSender.setPort(port); + javaMailSender.setJavaMailProperties(getMailProperties()); + javaMailSender.setDefaultEncoding("UTF-8"); + return javaMailSender; + } + private Properties getMailProperties() + { + Properties pt = new Properties(); + pt.put("mail.smtp.socketFactory.port", socketPort); + pt.put("mail.smtp.auth", auth); + pt.put("mail.smtp.starttls.enable", starttls); + pt.put("mail.smtp.starttls.required", startlls_required); + pt.put("mail.smtp.socketFactory.fallback",fallback); + pt.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + return pt; + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailService.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailService.java new file mode 100644 index 00000000..4fc0780e --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailService.java @@ -0,0 +1,5 @@ +package com.stockholm.main_project.auth.mail; + +public interface EmailService { + String sendSimpleMessage(String to)throws Exception; +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailServiceImpl.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailServiceImpl.java new file mode 100644 index 00000000..36cf292f --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/mail/EmailServiceImpl.java @@ -0,0 +1,85 @@ +package com.stockholm.main_project.auth.mail; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.util.Random; + +@Service +public class EmailServiceImpl implements EmailService{ + + @Autowired + JavaMailSender emailSender; + + public static final String ePw = createKey(); + + private MimeMessage createMessage(String to)throws Exception{ + System.out.println("보내는 대상 : "+ to); + System.out.println("인증 번호 : "+ePw); + MimeMessage message = emailSender.createMimeMessage(); + + message.addRecipients(MimeMessage.RecipientType.TO, to);//보내는 대상 + message.setSubject("이메일 인증 테스트");//제목 + + String msgg=""; + msgg+= "
"; + msgg+= "

안녕하세요 Stockholm입니다.

"; + msgg+= "
"; + msgg+= "

아래 코드를 복사해 입력해주세요

"; + msgg+= "
"; + msgg+= "

감사합니다.

"; + msgg+= "
"; + msgg+= "

"; + msgg+= "

회원가입 인증 코드입니다.

"; + msgg+= "
"; + msgg+= "CODE : "; + msgg+= ePw+"

"; + msgg+= "
"; + message.setText(msgg, "utf-8", "html");//내용 + message.setFrom(new InternetAddress("projectstockholm0@gmail.com","Stockholm"));//보내는 사람 + + return message; + } + + public static String createKey() { + StringBuffer key = new StringBuffer(); + Random rnd = new Random(); + + for (int i = 0; i < 8; i++) { // 인증코드 8자리 + int index = rnd.nextInt(3); // 0~2 까지 랜덤 + + switch (index) { + case 0: + key.append((char) ((int) (rnd.nextInt(26)) + 97)); + // a~z (ex. 1+97=98 => (char)98 = 'b') + break; + case 1: + key.append((char) ((int) (rnd.nextInt(26)) + 65)); + // A~Z + break; + case 2: + key.append((rnd.nextInt(10))); + // 0~9 + break; + } + } + return key.toString(); + } + @Override + public String sendSimpleMessage(String to)throws Exception { + // TODO Auto-generated method stub + MimeMessage message = createMessage(to); + try{//예외처리 + emailSender.send(message); + }catch(MailException es){ + es.printStackTrace(); + throw new IllegalArgumentException(); + } + return ePw; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java new file mode 100644 index 00000000..3f442e80 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/memberdetail/MemberDetailsService.java @@ -0,0 +1,72 @@ +package com.stockholm.main_project.auth.memberdetail; + +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Optional; + +@Component +public class MemberDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + private final CustomAuthorityUtils authorityUtils; + + public MemberDetailsService(MemberRepository memberRepository, CustomAuthorityUtils authorityUtils) { + this.memberRepository = memberRepository; + this.authorityUtils = authorityUtils; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Optional optionalMember = memberRepository.findByEmail(email); + Member findMember = optionalMember.orElseThrow(() -> new UsernameNotFoundException("User not found")); + + return new MemberDetails(findMember); + } + + private final class MemberDetails extends Member implements UserDetails { + + MemberDetails(Member member) { + setMemberId(member.getMemberId()); + setEmail(member.getEmail()); + setPassword(member.getPassword()); + setRoles(member.getRoles()); + } + + @Override + public Collection getAuthorities() { + return authorityUtils.createAuthorities(this.getRoles()); + } + + @Override + public String getUsername() { + return getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java new file mode 100644 index 00000000..542cc7bd --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/CustomAuthorityUtils.java @@ -0,0 +1,28 @@ +package com.stockholm.main_project.auth.utils; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class CustomAuthorityUtils { + + private final List USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER"); + private final List USER_ROLES_STRING = List.of("USER"); + + public List createRoles(String email) { + + return USER_ROLES_STRING; + } + + public List createAuthorities(List roles) { + List authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toList()); + return authorities; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/OAuth2MemberService.java b/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/OAuth2MemberService.java new file mode 100644 index 00000000..e56c44c2 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/auth/utils/OAuth2MemberService.java @@ -0,0 +1,57 @@ +package com.stockholm.main_project.auth.utils; + +import com.stockholm.main_project.auth.attribute.OAuthAttributes; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpSession; +import java.util.Collections; + +@Service +public class OAuth2MemberService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + private final HttpSession httpSession; + @Autowired + public OAuth2MemberService(MemberRepository memberRepository, HttpSession httpSession) { + this.memberRepository = memberRepository; + this.httpSession = httpSession; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest memberRequest) throws OAuth2AuthenticationException { + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oauthMember = delegate.loadUser(memberRequest); + + String registrationId = memberRequest.getClientRegistration().getRegistrationId(); + String userNameAttributeName = memberRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oauthMember.getAttributes()); + + Member member = saveOAuth(attributes); + + return new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority("USER")), + attributes.getAttributes(), + "email" + ); + + } + + private Member saveOAuth(OAuthAttributes attributes) { + Member member = memberRepository.findByEmail(attributes.getEmail()) + .orElse(attributes.toEntity()); + + return memberRepository.save(member); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/awss3/AwsS3Controller.java b/server/008main_project/src/main/java/com/stockholm/main_project/awss3/AwsS3Controller.java new file mode 100644 index 00000000..3758e953 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/awss3/AwsS3Controller.java @@ -0,0 +1,59 @@ +package com.stockholm.main_project.awss3; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import io.swagger.v3.oas.annotations.Operation; +import java.net.URL; + + +@RestController +@RequestMapping("/s3") +public class AwsS3Controller { + + private final AwsS3Service s3Service; + + public AwsS3Controller(AwsS3Service s3Service) { + this.s3Service = s3Service; + } + + + // 이미지 업로드 + @PostMapping("/upload/{folderName}") + @Operation(summary = "파일 업로드", description = "파일을 업로드합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "파일 업로드 성공", content = @Content(schema = @Schema(implementation = URL.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "500", description = "S3 파일 업로드 중 오류 발생") + }) + public URL uploadFile(@PathVariable String folderName, @RequestParam MultipartFile file) throws Exception { + return s3Service.uploadFile(folderName, file); + } + + @DeleteMapping("/delete/{folderName}/{fileName}") + @Operation(summary = "파일 삭제", description = "파일을 삭제합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "파일 삭제 성공"), + @ApiResponse(responseCode = "500", description = "S3 파일 삭제 중 오류 발생") + }) + public ResponseEntity deleteFile(@PathVariable String folderName, @PathVariable String fileName) { + s3Service.deleteFile(folderName, fileName); + return ResponseEntity.ok("파일이 삭제 되었습니다."); + } + + @GetMapping("/url/{folderName}/{fileName}") + @Operation(summary = "파일 URL 가져오기", description = "파일의 URL을 가져옵니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "URL 가져오기 성공", content = @Content(schema = @Schema(implementation = URL.class))), + @ApiResponse(responseCode = "500", description = "S3에서 파일 URL 검색 중 오류 발생") + }) + public ResponseEntity getFileUrl(@PathVariable String folderName, @PathVariable String fileName) { + URL url = s3Service.getFileUrl(folderName, fileName); + return ResponseEntity.ok(url); + } + +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/awss3/AwsS3Service.java b/server/008main_project/src/main/java/com/stockholm/main_project/awss3/AwsS3Service.java new file mode 100644 index 00000000..f39b3c0b --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/awss3/AwsS3Service.java @@ -0,0 +1,79 @@ +package com.stockholm.main_project.awss3; + + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.core.sync.RequestBody; + +import java.net.URL; + +@Service +@Slf4j +public class AwsS3Service { + private final String bucketName = "stockholm-server"; + private final S3Client s3Client; + + public AwsS3Service() { + try { + String awsAccessKeyId = System.getenv("AWS_S3_ACCESS_KEY"); + String awsSecretAccessKey = System.getenv("AWS_S3_SECRET_KEY"); + + AwsBasicCredentials awsCredentials = AwsBasicCredentials.create( + awsAccessKeyId, + awsSecretAccessKey + ); + + this.s3Client = S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) + .build(); + } catch (Exception e) { + throw new BusinessLogicException(ExceptionCode.AWS_CREDENTIALS_ERROR); + } + } + public URL uploadFile(String folderName, MultipartFile file) throws Exception { + try { + String fileName = folderName + "/" + file.getOriginalFilename(); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .contentType("image/jpeg") + .key(fileName) + .build(); + + s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + + return s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(fileName)); + } catch (Exception e) { + throw new BusinessLogicException(ExceptionCode.S3_UPLOAD_ERROR); + } + } + + public void deleteFile(String folderName, String fileName) { + try { + String fullFileName = folderName + "/" + fileName; + s3Client.deleteObject(builder -> builder.bucket(bucketName).key(fullFileName)); + } catch (Exception e) { + throw new BusinessLogicException(ExceptionCode.S3_DELETE_ERROR); + } + } + + public URL getFileUrl(String folderName, String fileName) { + try { + String fullFileName = folderName + "/" + fileName; + return s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(fullFileName)); + } catch (Exception e) { + throw new BusinessLogicException(ExceptionCode.S3_URL_RETRIEVE_ERROR); + } + } + } \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/controller/CommentController.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/controller/CommentController.java new file mode 100644 index 00000000..9e3f1092 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/controller/CommentController.java @@ -0,0 +1,66 @@ +package com.stockholm.main_project.board.commnet.controller; + +import com.stockholm.main_project.board.commnet.dto.CommentRequestDto; +import com.stockholm.main_project.board.commnet.dto.CommentResponseDto; +import com.stockholm.main_project.board.commnet.entity.Comment; +import com.stockholm.main_project.board.commnet.mapper.CommentMapper; +import com.stockholm.main_project.board.commnet.service.CommentService; +import com.stockholm.main_project.board.dto.responseDto.SingleBoardResponseDto; +import com.stockholm.main_project.board.entity.Board; + +import com.stockholm.main_project.board.service.BoardService; +import com.stockholm.main_project.member.entity.Member; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/boards/{boardId}/comments") +public class CommentController { + + private final CommentService commentService; + private final BoardService boardService; + private final CommentMapper mapper; + + + public CommentController(CommentService commentService, BoardService boardService, CommentMapper mapper) { + this.commentService = commentService; + this.boardService = boardService; + this.mapper = mapper; + } + + @PostMapping + public ResponseEntity postComment(@PathVariable long boardId, @RequestBody CommentRequestDto commentRequestDto, @AuthenticationPrincipal Member member){ + + Comment comment = mapper.commentRequestDtoToComment(commentRequestDto); + comment.setBoard(boardService.findBoard(boardId)); + comment.setMember(member); + + Comment createdComment = commentService.createComment(comment); + CommentResponseDto responseDto = mapper.commentToCommentResponseDto(createdComment); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + @PatchMapping("{commentId}") + public ResponseEntity updateComment(@PathVariable long boardId, + @PathVariable long commentId, + @RequestBody CommentRequestDto commentRequestDto, + @AuthenticationPrincipal Member member){ + + Comment comment = mapper.commentRequestDtoToComment(commentRequestDto); + comment.setCommentId(commentId); + + Comment updatedComment = commentService.updateComment(commentId, comment, member); + CommentResponseDto responseDto = mapper.commentToCommentResponseDto(updatedComment); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @DeleteMapping("{commentId}") + public ResponseEntity deleteComment(@PathVariable long boardId, @PathVariable long commentId, @AuthenticationPrincipal Member member) { + + commentService.deleteComment(commentId, member); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/dto/CommentRequestDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/dto/CommentRequestDto.java new file mode 100644 index 00000000..75f2a640 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/dto/CommentRequestDto.java @@ -0,0 +1,10 @@ +package com.stockholm.main_project.board.commnet.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CommentRequestDto { + private String content; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/dto/CommentResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/dto/CommentResponseDto.java new file mode 100644 index 00000000..d8a36a84 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/dto/CommentResponseDto.java @@ -0,0 +1,14 @@ +package com.stockholm.main_project.board.commnet.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CommentResponseDto { + private Long commentId; + private String member; + private String content; + private String createdAt; + private String ModifiedAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/entity/Comment.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/entity/Comment.java new file mode 100644 index 00000000..e4c5cadd --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/entity/Comment.java @@ -0,0 +1,41 @@ +package com.stockholm.main_project.board.commnet.entity; + +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.board.entity.Board; +import com.stockholm.main_project.member.entity.Member; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + + +@Entity +@Getter +@Setter +public class Comment extends Auditable { + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long commentId; + + @Column(length = 255, nullable = false) + private String content; + + @JoinColumn(name = "MEMBER_ID") + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull + private Member member; + + @JoinColumn(name = "BOARD_ID") + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + private Board board; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/mapper/CommentMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/mapper/CommentMapper.java new file mode 100644 index 00000000..db9f848c --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/mapper/CommentMapper.java @@ -0,0 +1,16 @@ +package com.stockholm.main_project.board.commnet.mapper; + +import com.stockholm.main_project.board.commnet.dto.CommentRequestDto; +import com.stockholm.main_project.board.commnet.dto.CommentResponseDto; +import com.stockholm.main_project.board.commnet.entity.Comment; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + + Comment commentRequestDtoToComment(CommentRequestDto commentRequestDto); + + @Mapping(source = "member.name", target = "member") + CommentResponseDto commentToCommentResponseDto(Comment comment); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/repository/CommentRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/repository/CommentRepository.java new file mode 100644 index 00000000..d738b187 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/repository/CommentRepository.java @@ -0,0 +1,15 @@ +package com.stockholm.main_project.board.commnet.repository; + +import com.stockholm.main_project.board.commnet.entity.Comment; +import com.stockholm.main_project.board.entity.Board; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface CommentRepository extends JpaRepository { + + List findAllByBoardBoardId(long boardId); + Optional findByCommentId(Long commentId); + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/service/CommentService.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/service/CommentService.java new file mode 100644 index 00000000..a3fe6e20 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/commnet/service/CommentService.java @@ -0,0 +1,64 @@ +package com.stockholm.main_project.board.commnet.service; + +import com.stockholm.main_project.board.commnet.dto.CommentRequestDto; +import com.stockholm.main_project.board.commnet.entity.Comment; +import com.stockholm.main_project.board.commnet.repository.CommentRepository; +import com.stockholm.main_project.board.entity.Board; +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.member.entity.Member; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class CommentService { + + private final CommentRepository commentRepository; + + public CommentService(CommentRepository commentRepository) { + this.commentRepository = commentRepository; + } + + public Comment createComment(Comment comment){ + return commentRepository.save(comment); + } + + public Comment updateComment(long commentId ,Comment comment, Member member){ + Comment existingComment = findComment(commentId); + + if (existingComment.getMember().getMemberId() != member.getMemberId()) { + throw new BusinessLogicException(ExceptionCode.INVALID_FAILED); + } + + existingComment.setContent(comment.getContent()); + + return commentRepository.save(existingComment); + + } + + public List findComments(long commentId) { + return commentRepository.findAllByBoardBoardId(commentId); + } + + public void deleteComment (long commentId, Member member) { + Comment comment = findComment(commentId); + + if (comment.getMember().getMemberId() != member.getMemberId()) { + throw new BusinessLogicException(ExceptionCode.INVALID_FAILED); + } + + commentRepository.delete(comment); + } + + public Comment findComment(long commentId) { + return findVerifiedComment(commentId); + } + + public Comment findVerifiedComment(long commentId) { + Optional optionalComment = commentRepository.findByCommentId(commentId); + return optionalComment.orElseThrow(() -> new BusinessLogicException(ExceptionCode.COMMENT_NOT_FOUND)); + } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/controller/BoardController.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/controller/BoardController.java new file mode 100644 index 00000000..0de6d76d --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/controller/BoardController.java @@ -0,0 +1,143 @@ +package com.stockholm.main_project.board.controller; + +import com.stockholm.main_project.board.commnet.dto.CommentResponseDto; +import com.stockholm.main_project.board.commnet.entity.Comment; +import com.stockholm.main_project.board.commnet.service.CommentService; +import com.stockholm.main_project.board.dto.responseDto.BoardCommentDto; +import com.stockholm.main_project.board.service.BoardService; +import com.stockholm.main_project.board.dto.BoardRequestDto; +import com.stockholm.main_project.board.dto.responseDto.AllBoardResponseDto; +import com.stockholm.main_project.board.dto.responseDto.SingleBoardResponseDto; +import com.stockholm.main_project.board.entity.Board; // Board 클래스를 참조하도록 수정 +import com.stockholm.main_project.board.mapper.BoardMapper; +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.service.MemberService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/boards") +public class BoardController { + + + private final BoardService boardService; + + private final BoardMapper mapper; + private final CommentService commentService; + + public BoardController(BoardService boardService, BoardMapper mapper, CommentService commentService) { + this.boardService = boardService; + this.mapper = mapper; + this.commentService = commentService; + } + + // 게시물 생성 + @PostMapping + @Operation(summary = "게시물 생성", description = "새로운 게시물을 생성합니다.", tags = { "Board" }) + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Created", content = @Content(schema = @Schema(implementation = Board.class))), + @ApiResponse(responseCode = "400", description = "Bad Request"), + @ApiResponse(responseCode = "405", description = "Method Not Allowed"), + @ApiResponse(responseCode = "500", description = "Internal Server Error") + }) + public ResponseEntity createBoard(@Valid @RequestBody BoardRequestDto boardPostDto, @AuthenticationPrincipal Member member) throws Exception { + Board boardToCreate = mapper.boardRequestToBoard(boardPostDto); + boardToCreate.setMember(member); + + Board createdBoard = boardService.createBoard(boardToCreate); + SingleBoardResponseDto responseDto = mapper.boardToBoardResponseDto(createdBoard); + + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + @Operation(summary = "게시물 정보 변경", description = "게시물을 수정합니다.", tags = { "Board" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = BoardRequestDto.class))) + @ApiResponse(responseCode = "400", description = "BAD REQUEST") + @ApiResponse(responseCode = "404", description = "NOT FOUND") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @PatchMapping("{boardId}") + public ResponseEntity updateBoard(@Valid @PathVariable long boardId,@RequestBody BoardRequestDto boardRequestDto, + @AuthenticationPrincipal Member member) throws Exception { + Board boardToUpdate = mapper.boardRequestToBoard(boardRequestDto); + + Board board = boardService.updateBoard(boardId, boardToUpdate, member); + + SingleBoardResponseDto responseDto = mapper.boardToBoardResponseDto(board); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @Operation(summary = "게시물 정보 조회", description = "게시물을 조회합니다.", tags = { "Board" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = SingleBoardResponseDto.class))) + @ApiResponse(responseCode = "400", description = "BAD REQUEST") + @ApiResponse(responseCode = "404", description = "NOT FOUND") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @GetMapping("{boardId}") + public ResponseEntity getBoard(@PathVariable long boardId){ + Board response = boardService.findBoard(boardId); + SingleBoardResponseDto responseDto = mapper.boardToBoardResponseDto(response); + + List comments = commentService.findComments(boardId); + List boardCommentDtos = comments + .stream() + .map(mapper::boardCommentToBoardCommentsDto) + .collect(Collectors.toList()); + responseDto.setComments(boardCommentDtos); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + @Operation(summary = "전체 게시물 조회", description = "모든 게시물을 조회합니다.", tags = { "Board" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = AllBoardResponseDto.class))) + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @GetMapping + public ResponseEntity> getBoards(){ + List foundBoards = boardService.getAllBoards(); + + List responseDtos = foundBoards.stream() + .map(board -> { + AllBoardResponseDto responseDto = mapper.boardToAllBoardResponseDto(board); + List comments = commentService.findComments(board.getBoardId()); + List boardCommentDtos = comments.stream() + .map(mapper::boardCommentToBoardCommentsDto) + .collect(Collectors.toList()); + responseDto.setComments(boardCommentDtos); + return responseDto; + }) + .collect(Collectors.toList()); + return new ResponseEntity<>(responseDtos, HttpStatus.OK); + } + + @Operation(summary = "게시물 삭제", description = "게시물을 삭제합니다.", tags = { "Board" }) + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "No Content"), + @ApiResponse(responseCode = "400", description = "Bad Request"), + @ApiResponse(responseCode = "404", description = "BOARD_NOT_FOUND"), + @ApiResponse(responseCode = "500", description = "Internal Server Error")}) + @DeleteMapping("{boardId}") + public ResponseEntity deleteBoard(@PathVariable long boardId, @AuthenticationPrincipal Member member){ + boardService.deleteBoard(boardId, member); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + + } + +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/BoardRequestDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/BoardRequestDto.java new file mode 100644 index 00000000..dd3580e8 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/BoardRequestDto.java @@ -0,0 +1,26 @@ +package com.stockholm.main_project.board.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Getter +@Setter +@NoArgsConstructor +public class BoardRequestDto { + @Schema(description = "게시물 제목", defaultValue = "TestBoard") + @NotBlank(message = "제목을 입력해 주세요") + @Size(max = 30, message = "제목의 길이는 30자를 넘을 수 없습니다") + private String title; + + @Schema(description = "게시물 내용", defaultValue = "TestContent") + @NotBlank(message = "내용을 입력해 주세요") + @Size(max = 100, message = "내용은 100자를 넘을 수 없습니다") + private String content; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/AllBoardResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/AllBoardResponseDto.java new file mode 100644 index 00000000..cbdb989d --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/AllBoardResponseDto.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.board.dto.responseDto; + +import com.stockholm.main_project.board.commnet.dto.CommentResponseDto; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +public class AllBoardResponseDto { + private Long boardId; + private String title; + private String content; + private String member; + private List comments; + private String createdAt; + private String modifiedAt; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/BoardCommentDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/BoardCommentDto.java new file mode 100644 index 00000000..540b9093 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/BoardCommentDto.java @@ -0,0 +1,14 @@ +package com.stockholm.main_project.board.dto.responseDto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BoardCommentDto { + private long commentId; // 추가 + private String content; + private String member; + private String createdAt; + private String ModifiedAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/SingleBoardResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/SingleBoardResponseDto.java new file mode 100644 index 00000000..26d9bd2c --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/dto/responseDto/SingleBoardResponseDto.java @@ -0,0 +1,24 @@ +package com.stockholm.main_project.board.dto.responseDto; + + + +import com.stockholm.main_project.board.commnet.dto.CommentResponseDto; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +// 게시판 응답 형식 클래스 +@Getter +@Setter +public class SingleBoardResponseDto { + private Long boardId; + private String title; + private String content; + private String member; + private String createdAt; + private String modifiedAt; + private List comments; + + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/entity/Board.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/entity/Board.java new file mode 100644 index 00000000..f8ae2a4b --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/entity/Board.java @@ -0,0 +1,42 @@ +package com.stockholm.main_project.board.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Board extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long boardId; + + @Column + private String title; + + @Column + private String content; + + @JoinColumn(name = "MEMBER_ID") + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull + private Member member; + + private String imageUrl; // 이미지 URL + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/mapper/BoardMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/mapper/BoardMapper.java new file mode 100644 index 00000000..3b1d1bf6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/mapper/BoardMapper.java @@ -0,0 +1,30 @@ +package com.stockholm.main_project.board.mapper; + +import com.stockholm.main_project.board.commnet.entity.Comment; +import com.stockholm.main_project.board.dto.BoardRequestDto; +import com.stockholm.main_project.board.dto.responseDto.AllBoardResponseDto; +import com.stockholm.main_project.board.dto.responseDto.BoardCommentDto; +import com.stockholm.main_project.board.dto.responseDto.SingleBoardResponseDto; +import com.stockholm.main_project.board.entity.Board; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface BoardMapper { + + + + Board boardRequestToBoard(BoardRequestDto requestBody); + + @Mapping(source = "member.name", target = "member") + SingleBoardResponseDto boardToBoardResponseDto(Board board); + + @Mapping(source = "member.name", target = "member") + AllBoardResponseDto boardToAllBoardResponseDto(Board board); + + @Mapping(source = "member.name", target = "member") + BoardCommentDto boardCommentToBoardCommentsDto(Comment comment); + +} + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/repository/BoardRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/repository/BoardRepository.java new file mode 100644 index 00000000..b2dee158 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/repository/BoardRepository.java @@ -0,0 +1,13 @@ +package com.stockholm.main_project.board.repository; + +import com.stockholm.main_project.board.entity.Board; +import com.stockholm.main_project.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface BoardRepository extends JpaRepository { + Optional findByBoardId(Long boardId); +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/board/service/BoardService.java b/server/008main_project/src/main/java/com/stockholm/main_project/board/service/BoardService.java new file mode 100644 index 00000000..bd0381f6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/board/service/BoardService.java @@ -0,0 +1,71 @@ +package com.stockholm.main_project.board.service; + +import com.stockholm.main_project.awss3.AwsS3Service; +import com.stockholm.main_project.board.repository.BoardRepository; +import com.stockholm.main_project.board.entity.Board; +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.member.entity.Member; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URL; +import java.util.List; +import java.util.Optional; + +@Service +@Slf4j +public class BoardService { + + @Autowired + private BoardRepository boardRepository; + + public Board createBoard(Board board) { + + return boardRepository.save(board); + } + + public Board updateBoard(long boardId, Board updatedBoard, Member member) { + Board board = findBoard(boardId); + + if (board.getMember().getMemberId() != member.getMemberId()) { + throw new BusinessLogicException(ExceptionCode.INVALID_FAILED); + } + + board.setTitle(updatedBoard.getTitle()); + board.setContent(updatedBoard.getContent()); + + return boardRepository.save(board); + } + + public Board findBoard(long boardId) { + + return findVerifiedBoard(boardId); + } + + public List getAllBoards() { + return boardRepository.findAll(); + } + + public void deleteBoard(long boardId, Member member) { + Board board = findBoard(boardId); + + if (board.getMember().getMemberId() != member.getMemberId()) { + throw new BusinessLogicException(ExceptionCode.INVALID_FAILED); + } + + boardRepository.delete(board); + } + + public Board findVerifiedBoard(long boardId) { + + Optional optionalBoard = + boardRepository.findByBoardId(boardId); + Board findBoard = + optionalBoard.orElseThrow(() -> + new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); + return findBoard; + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/controller/CashController.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/controller/CashController.java new file mode 100644 index 00000000..73b46d55 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/controller/CashController.java @@ -0,0 +1,94 @@ +package com.stockholm.main_project.cash.controller; + +import com.stockholm.main_project.cash.dto.CashPatchDto; +import com.stockholm.main_project.cash.dto.CashPostDto; +import com.stockholm.main_project.cash.dto.CashResponseDto; +import com.stockholm.main_project.cash.entity.Cash; +import com.stockholm.main_project.cash.mapper.CashMapper; +import com.stockholm.main_project.cash.service.CashService; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.service.MemberService; +import com.stockholm.main_project.stock.service.StockHoldService; +import com.stockholm.main_project.stock.service.StockOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/cash") +public class CashController { + + private final CashMapper mapper; + private final CashService cashService; + private final MemberService memberService; + private final StockHoldService stockHoldService; + private final StockOrderService stockOrderService; + + public CashController(CashMapper mapper, CashService cashService, MemberService memberService, StockHoldService stockHoldService, StockOrderService stockOrderService) { + this.mapper = mapper; + this.cashService = cashService; + this.memberService = memberService; + this.stockHoldService = stockHoldService; + this.stockOrderService = stockOrderService; + } + @PostMapping + @Operation(summary = "현금 정보 생성", description = "새로운 현금 정보를 생성합니다.", tags = { "Cash" }) + @ApiResponse(responseCode = "201", description = "Created", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CashResponseDto.class))) + @ApiResponse(responseCode = "400", description = "이미 보유한 현금이 있습니다.") + @ApiResponse(responseCode = "401", description = "Not Enough Money") + public ResponseEntity postCash(@Schema(implementation = CashPostDto.class)@Valid @RequestBody CashPostDto cashPostDto, + @AuthenticationPrincipal Member member){ + + Cash cashToCreate = mapper.cashPostToCash(cashPostDto); + + cashToCreate.setMember(member); + + Cash createdCash = cashService.createCash(cashToCreate); + CashResponseDto responseDto = mapper.cashToCashResponseDto(createdCash); + + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + @PatchMapping("{cashId}") + @Operation(summary = "현금 정보 업데이트", description = "현금 정보를 업데이트합니다.", tags = { "Cash" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CashResponseDto.class))) + @ApiResponse(responseCode = "400", description = "Bad Request") + @ApiResponse(responseCode = "401", description = "Invalid Cash") + @ApiResponse(responseCode = "404", description = "Not Found") + public ResponseEntity patchCash(@Schema(implementation = CashPatchDto.class)@PathVariable long cashId, @Valid @RequestBody CashPatchDto requestBody, + @AuthenticationPrincipal Member member){ + + Cash cashToUpdate = mapper.cashPatchToCash(requestBody); + + cashToUpdate.setMember(member); + + requestBody.setCashId(cashId); + + Cash cash = cashService.updateCash(cashId, member, requestBody); + stockHoldService.deleteStockHolds(member.getMemberId()); + stockOrderService.deleteStockOrders(member); + + return new ResponseEntity<>(mapper.cashToCashResponseDto(cash), HttpStatus.OK); + } + + @Operation(summary = "현금 정보 조회", description = "현금 정보를 조회합니다.", tags = { "Cash" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CashResponseDto.class))) + @ApiResponse(responseCode = "401", description = "Invalid Cash") + @ApiResponse(responseCode = "404", description = "Not Found") + @GetMapping + private ResponseEntity getCash(@AuthenticationPrincipal Member member){ + Cash response = cashService.findCash(member); + + return new ResponseEntity<>(mapper.cashToCashResponseDto(response), HttpStatus.OK); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashPatchDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashPatchDto.java new file mode 100644 index 00000000..002164d7 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashPatchDto.java @@ -0,0 +1,23 @@ +package com.stockholm.main_project.cash.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.Pattern; + + +@Getter +@Setter +public class CashPatchDto { + + @Schema(description = "CashId", defaultValue = "1") + private long cashId; + @Schema(description = "금액", defaultValue = "5000000") + @Min(value = 1000000, message = "금액은 최소 1,000,000 이상이어야 합니다.") // 최소값 설정 + @Max(value = 500000000, message = "금액은 최대 500,000,000 이하여야 합니다.") // 최대값 설정 + private long money; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashPostDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashPostDto.java new file mode 100644 index 00000000..a219c644 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashPostDto.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.cash.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Getter +@Setter +public class CashPostDto { + + + @Schema(description = "금액", defaultValue = "5000000") + @Min(value = 1000000, message = "금액은 최소 1,000,000 이상이어야 합니다.") // 최소값 설정 + @Max(value = 500000000, message = "금액은 최대 500,000,000 이하여야 합니다.") // 최대값 설정 + private long money; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashResponseDto.java new file mode 100644 index 00000000..ffff8818 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/dto/CashResponseDto.java @@ -0,0 +1,20 @@ +package com.stockholm.main_project.cash.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class CashResponseDto { + @Schema(description = "CashId", defaultValue = "1") + private long cashId; + @Schema(description = "금액", defaultValue = "5000000") + private long money; + @Schema(description = "생성 시간", defaultValue = "2023-09-04T12:00:00") + private LocalDateTime createdAt; + @Schema(description = "수정 시간", defaultValue = "2023-09-04T12:00:00") + private LocalDateTime modifiedAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/entity/Cash.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/entity/Cash.java new file mode 100644 index 00000000..e4a93259 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/entity/Cash.java @@ -0,0 +1,33 @@ +package com.stockholm.main_project.cash.entity; + +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Cash extends Auditable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long cashId; + + @Column(nullable = false) + private long money; + + @JoinColumn(name = "MEMBER_ID") + @OneToOne(fetch = FetchType.LAZY) + private Member member; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/mapper/CashMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/mapper/CashMapper.java new file mode 100644 index 00000000..128a4fef --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/mapper/CashMapper.java @@ -0,0 +1,20 @@ +package com.stockholm.main_project.cash.mapper; + +import com.stockholm.main_project.cash.dto.CashPatchDto; +import com.stockholm.main_project.cash.dto.CashPostDto; +import com.stockholm.main_project.cash.dto.CashResponseDto; +import com.stockholm.main_project.cash.entity.Cash; +import com.stockholm.main_project.member.dto.MemberPatchDto; +import com.stockholm.main_project.member.dto.MemberPostDto; +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface CashMapper { + + Cash cashPostToCash(CashPostDto requestBody); + + Cash cashPatchToCash(CashPatchDto requestBody); + CashResponseDto cashToCashResponseDto(Cash cash); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/repository/CashRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/repository/CashRepository.java new file mode 100644 index 00000000..de12ccdc --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/repository/CashRepository.java @@ -0,0 +1,13 @@ +package com.stockholm.main_project.cash.repository; + +import com.stockholm.main_project.cash.entity.Cash; +import com.stockholm.main_project.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CashRepository extends JpaRepository { + + + List findByMemberAndMoney(Member member, long money); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/cash/service/CashService.java b/server/008main_project/src/main/java/com/stockholm/main_project/cash/service/CashService.java new file mode 100644 index 00000000..14e08a65 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/cash/service/CashService.java @@ -0,0 +1,81 @@ +package com.stockholm.main_project.cash.service; + +import com.stockholm.main_project.cash.dto.CashPatchDto; +import com.stockholm.main_project.cash.entity.Cash; +import com.stockholm.main_project.cash.repository.CashRepository; +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.member.entity.Member; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@Slf4j +public class CashService { + + private final CashRepository cashRepository; + + + public CashService(CashRepository cashRepository) { + this.cashRepository = cashRepository; + } + + public Cash createCash(Cash cash) { + + if (isCashAlreadyExists(cash)) { + throw new BusinessLogicException(ExceptionCode.CASH_DUPLICATION); + } + + Cash saveCash = cashRepository.save(cash); + System.out.println("# Create Cash"); + + return saveCash; + } + + public Cash updateCash(long cashId, Member member, CashPatchDto patchDto){ + Cash cash = findCash(member); + +// validateAuthor(cash, member); + cash.setMoney(patchDto.getMoney()); + + return cashRepository.save(cash); + + } + + public Cash findCash(Member member) { + + Cash cash = member.getCash(); + + if (cash == null) { + throw new BusinessLogicException(ExceptionCode.INVALID_CASH); + } + + return cash; + } + + public void checkCash(long price, Member member) { + if(price > member.getCash().getMoney()) + throw new BusinessLogicException(ExceptionCode.NOT_ENOUGH_MONEY); + else + return; + } + + private void validateAuthor(Cash cash, Member member) { + + if (!cash.getMember().equals(member)) { + throw new BusinessLogicException(ExceptionCode.INVALID_CASH); + } + } + private boolean isCashAlreadyExists(Cash cash) { + Member member = cash.getMember(); + long money = cash.getMoney(); + + List existingCashList = cashRepository.findByMemberAndMoney(member, money); + + return !existingCashList.isEmpty(); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java b/server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java new file mode 100644 index 00000000..32c438bc --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/config/SecurityConfiguration.java @@ -0,0 +1,132 @@ +package com.stockholm.main_project.config; + +import com.stockholm.main_project.auth.filter.JwtAuthenticationFilter; +import com.stockholm.main_project.auth.filter.JwtVerificationFilter; +import com.stockholm.main_project.auth.handler.MemberAuthenticationFailureHandler; +import com.stockholm.main_project.auth.handler.MemberAuthenticationSuccessHandler; +import com.stockholm.main_project.auth.handler.OAuth2AuthenticationSuccessHandler; +import com.stockholm.main_project.auth.jwt.JwtTokenizer; +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.auth.utils.OAuth2MemberService; +import com.stockholm.main_project.member.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + private final JwtTokenizer jwtTokenizer; + private final CustomAuthorityUtils authorityUtils; + @Autowired + public final OAuth2MemberService oAuth2MemberService; + + private final MemberService memberService; + + public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils, OAuth2MemberService oAuth2MemberService, @Lazy MemberService memberService) { + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + this.oAuth2MemberService = oAuth2MemberService; + this.memberService = memberService; + } + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .headers().frameOptions().sameOrigin() //H2 웹 콘솔에 정상적으로 접근 가능하도록 설정 + .and() + .cors().configurationSource(corsConfigurationSource())// CORS 설정을 추가 + .and() + .csrf().disable() //CSRF 공격에 대한 설정 + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)를 통해서 세션을 생성하지 않도록 설정 + .and() + .formLogin().disable() // JSON 포맷 전달 방식 사용을 위해 비활성화 + .httpBasic().disable() // request 전송마다 로그인 정보를 받지 않을 것임으로 비활성화 + .apply(new CustomFilterConfigurer()) + .and() + .authorizeHttpRequests(authorize -> authorize + .antMatchers("members/login").permitAll() + .antMatchers(HttpMethod.POST, "/cash").hasRole("USER") + .antMatchers(HttpMethod.PATCH, "/cash/{cashId}").hasRole("USER") + .antMatchers(HttpMethod.GET, "/cash").hasRole("USER") + .antMatchers(HttpMethod.POST, "/stock/buy").hasRole("USER") + .antMatchers(HttpMethod.POST, "/stock/sell").hasRole("USER") + .antMatchers(HttpMethod.GET, "/stock/stockholds").hasRole("USER") + .antMatchers(HttpMethod.GET, "/stock/stockorders").hasRole("USER") + .antMatchers(HttpMethod.DELETE, "/stock/stockorders").hasRole("USER") + .antMatchers(HttpMethod.GET, "long-polling/listen").hasRole("USER") + .antMatchers(HttpMethod.POST, "/api/boards").hasRole("USER") + .antMatchers(HttpMethod.PATCH, "/api/boards/{boardId}").hasRole("USER") + .antMatchers(HttpMethod.DELETE, "/api/boards/{boardId}").hasRole("USER") + .antMatchers(HttpMethod.GET, "api/boards").permitAll() + .antMatchers(HttpMethod.GET, "api/boards/{boardId}").permitAll() //질문을 선택해 조회하는 기능은 인증된 사용자에게만 혀용 + .antMatchers(HttpMethod.POST, "/api/boards/{boardId}/comment").hasRole("USER") + .antMatchers(HttpMethod.PATCH, "/api/boards/{boardId}/comment/{commentId}").hasRole("USER") + .antMatchers(HttpMethod.DELETE, "/api/boards/{boardId}/comment/{commentId}").hasRole("USER") + .anyRequest().permitAll() + ) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint() + .userService(oAuth2MemberService) + .and() + .successHandler(new OAuth2AuthenticationSuccessHandler(jwtTokenizer, authorityUtils, memberService)) + ); + + return httpSecurity.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowCredentials(true); + configuration.setAllowedOrigins(List.of( + "http://seb008stockholm.s3-website.ap-northeast-2.amazonaws.com/", + "http://localhost:5173", "http://localhost:3000", "http://localhost:8080" + ,"chrome-extension://ggnhohnkfcpcanfekomdkjffnfcjnjam" + )); + configuration.setAllowedMethods(List.of("GET","POST", "PATCH", "DELETE")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + public class CustomFilterConfigurer extends AbstractHttpConfigurer { + @Override + public void configure(HttpSecurity builder) throws Exception { + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + + JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); + jwtAuthenticationFilter.setFilterProcessesUrl("/members/login"); + jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler()); + jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); + builder.addFilter(jwtAuthenticationFilter); + + JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils, memberService); + + builder + .addFilter(jwtAuthenticationFilter) + .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); + } + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/config/SwaggerConfig.java b/server/008main_project/src/main/java/com/stockholm/main_project/config/SwaggerConfig.java new file mode 100644 index 00000000..cc3252ec --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/config/SwaggerConfig.java @@ -0,0 +1,53 @@ +package com.stockholm.main_project.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; + +@OpenAPIDefinition( + info = @Info(title = "Stockholm API 명세서", + description = "모의주식 투자 연습 사이트", + version = "v1")) +@RequiredArgsConstructor +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + + + // SecuritySecheme명 + String jwtSchemeName = "jwtAuth"; + // API 요청헤더에 인증정보 포함 + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + // SecuritySchemes 등록 + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) // HTTP 방식 + .scheme("bearer") + .bearerFormat("JWT")); // 토큰 형식을 지정하는 임의의 문자(Optional) + + return new OpenAPI() + .addSecurityItem(securityRequirement) + .components(components); + } + + @Bean + public GroupedOpenApi chatOpenApi() { + String[] paths = {"/**"}; + + return GroupedOpenApi.builder() + .group("stockholm API v1") + .pathsToMatch(paths) + .build(); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/exception/BusinessLogicException.java b/server/008main_project/src/main/java/com/stockholm/main_project/exception/BusinessLogicException.java new file mode 100644 index 00000000..5258f9ed --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/exception/BusinessLogicException.java @@ -0,0 +1,13 @@ +package com.stockholm.main_project.exception; + +import lombok.Getter; + +public class BusinessLogicException extends RuntimeException { + @Getter + private ExceptionCode exceptionCode; + + public BusinessLogicException(ExceptionCode exceptionCode) { + super(exceptionCode.getMessage()); + this.exceptionCode = exceptionCode; + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/exception/ExceptionCode.java b/server/008main_project/src/main/java/com/stockholm/main_project/exception/ExceptionCode.java new file mode 100644 index 00000000..90f2c843 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/exception/ExceptionCode.java @@ -0,0 +1,40 @@ +package com.stockholm.main_project.exception; + +import com.stockholm.main_project.stock.entity.StockAsBi; +import lombok.Getter; + +public enum ExceptionCode { + MEMBER_NOT_FOUND(404, "회원을 찾을 수 없습니다"), + INVALID_EMAIL(404, "유효하지 않은 이메일 형식입니다"), + INVALID_NAME(400, "이름이 유효하지 않습니다"), + INVALID_CASH(404, "금액을 조회할 수 없습니다."), + EMAIL_DUPLICATION(400, "이미 존재하는 이메일입니다."), + CASH_DUPLICATION(400, "이미 보유한 현금이 있습니다."), + INVALID_PASSWORD(404, "비밀번호가 일치하지 않거나 유효하지 않습니다."), + BOARD_NOT_FOUND(404, "게시물을 찾을 수 없습니다."), + COMMENT_NOT_FOUND(404, "댓글을 찾을 수 없습니다."), + INVALID_FAILED(404, "작성자만 접근할 수 있습닌다."), + STOCKASBI_NOT_FOUND(404, "호가 정보를 찾을 수 없습니다"), + STOCKHOLD_NOT_FOUND(404, "보유 주식 정보가 없습니다."), + INSUFFICIENT_STOCK(422,"보유 주식이 부족합니다"), + NOT_ENOUGH_MONEY(422, "보유 금액이 부족합니다"), + STOCKORDER_NOT_FOUND(404, "주식 거래내역이 존재하지 않습니다"), + STOCKORDER_PERMISSION_DENIED(400,"잘못된 삭제 요청입니다."), + STOCKORDER_ALREADY_FINISH(400, "이미 완료된 거래입니다"), + AWS_CREDENTIALS_ERROR(401, "AWS 인증 오류"), + S3_UPLOAD_ERROR(500, "S3 파일 업로드 중 오류 발생"), + S3_DELETE_ERROR(500, "S3 파일 삭제 중 오류 발생"), + S3_URL_RETRIEVE_ERROR(500, "S3에서 파일 URL 검색 중 오류 발생"); + + + @Getter + private int status; + + @Getter + private String message; + + ExceptionCode(int code, String message) { + this.status = code; + this.message = message; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java new file mode 100644 index 00000000..fc63c964 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/controller/MemberController.java @@ -0,0 +1,97 @@ +package com.stockholm.main_project.member.controller; + +import com.stockholm.main_project.member.dto.MemberPatchDto; +import com.stockholm.main_project.member.dto.MemberPostDto; +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.mapper.MemberMapper; +import com.stockholm.main_project.member.service.MemberService; +import com.stockholm.main_project.swaggersample.HelloResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/members") +@Slf4j +public class MemberController { + private final MemberService memberService; + private final MemberMapper mapper; + + + public MemberController(MemberService memberService, MemberMapper mapper) { + this.memberService = memberService; + this.mapper = mapper; + } + + @Operation(summary = "회원가입", description = "자체 회원가입 요청이 POST됩니다.", tags = { "Member" }) + @ApiResponse(responseCode = "201", description = "CREATED", + content = @Content(schema = @Schema(implementation = MemberResponseDto.class))) + @ApiResponse(responseCode = "400", description = "EMAIL_DUPLICATION") + @ApiResponse(responseCode = "404", description = "INVALID_PASSWORD") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @PostMapping + public ResponseEntity postMember(@Schema(implementation = MemberPostDto.class)@Valid @RequestBody MemberPostDto memberPostDto){ + Member member = mapper.memberPostToMember(memberPostDto); + + Member response = memberService.createMember(member); + + return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), + HttpStatus.CREATED); + } + + @Operation(summary = "회원 정보 변경", description = "가입한 계정의 이름을 PATCH합니다.", tags = { "Member" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = MemberResponseDto.class))) + @ApiResponse(responseCode = "400", description = "BAD REQUEST") + @ApiResponse(responseCode = "404", description = "MEMBER NOT FOUND") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @PatchMapping + private ResponseEntity patchMember(@Schema(implementation = MemberPatchDto.class)@RequestBody MemberPatchDto memberPatchDto, @AuthenticationPrincipal Member member){ + + memberPatchDto.setMemberId(member.getMemberId()); + + Member patchedMember = new Member(); + patchedMember.setMemberId(memberPatchDto.getMemberId()); + patchedMember.setName(memberPatchDto.getName()); + patchedMember.setEmail(memberPatchDto.getEmail()); + + Member response = memberService.updateMember(patchedMember); + + return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK); + } + + + @Operation(summary = "회원 조회", description = "가입한 계정 중 하나가 GET됩니다.", tags = { "Member" }) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = MemberResponseDto.class))) + @ApiResponse(responseCode = "400", description = "BAD REQUEST") + @ApiResponse(responseCode = "404", description = "NOT FOUND") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @GetMapping + private ResponseEntity getMember(@AuthenticationPrincipal Member member){ + Member response = memberService.findMember(member.getMemberId()); + + return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK); + } + + @Operation(summary = "회원 삭제", description = "가입한 계정을 DELETE합니다.", tags = { "Member" }) + @ApiResponse(responseCode = "204", description = "NO CONTENT") + @ApiResponse(responseCode = "404", description = "NOT FOUND") + @ApiResponse(responseCode = "400", description = "BAD REQUEST") + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + @DeleteMapping + private ResponseEntity deleteMember(@AuthenticationPrincipal Member member){ + memberService.deleteMember(member.getMemberId()); + + return new ResponseEntity(HttpStatus.NO_CONTENT); + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java new file mode 100644 index 00000000..68b94853 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPatchDto.java @@ -0,0 +1,17 @@ +package com.stockholm.main_project.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MemberPatchDto { + @Schema(description = "MemberId", defaultValue = "1") + private long memberId; + @Schema(description = "Email 변경 불가", defaultValue = "Test@example.com") + private String email; + @Schema(description = "변경을 원하는 이름", defaultValue = "TestName12") + private String name; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java new file mode 100644 index 00000000..36e58b1e --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberPostDto.java @@ -0,0 +1,37 @@ +package com.stockholm.main_project.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.constraints.*; + +@Getter +@Setter +public class MemberPostDto { + @Email + @NotBlank + @Pattern(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]+$", + message = "올바른 이메일 구성이 아닙니다.") + @Schema(description = "Email", defaultValue = "Test@example.com") + private String email; + + @NotBlank + @Schema(description = "이름", defaultValue = "TestName") + private String name; + + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$", + message = "1자 이상의 문자, 1개 이상의 숫자, 1개 이상의 특수문자를 포함하고 8자리 이상이어야 합니다.") + @NotBlank + @Schema(description = "비밀번호", defaultValue = "test123!@#") + private String password; + + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$", + message = "1자 이상의 문자, 1개 이상의 숫자, 1개 이상의 특수문자를 포함하고 8자리 이상이어야 합니다.") + @NotBlank + @Schema(description = "비밀번호 확인", defaultValue = "test123!@#") + private String confirmPassword; + + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java new file mode 100644 index 00000000..94ae1c2f --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/dto/MemberResponseDto.java @@ -0,0 +1,22 @@ +package com.stockholm.main_project.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class MemberResponseDto { + @Schema(description = "MemberId", defaultValue = "1") + public long memberId; + @Schema(description = "Email", defaultValue = "Test@example.com") + public String email; + @Schema(description = "이름", defaultValue = "TestName") + private String name; + @Schema(description = "CashID", defaultValue = "1") + private String cash; + @Schema(description = "생성 시간", defaultValue = "LocalDateTime") + private LocalDateTime createdAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java new file mode 100644 index 00000000..1568826e --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/entity/Member.java @@ -0,0 +1,71 @@ +package com.stockholm.main_project.member.entity; + +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.cash.entity.Cash; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Member extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long memberId; + + @Column(length = 30, nullable = false) + private String email; + + @Column(length = 10, nullable = false) + private String name; + + @Column(length = 255, nullable = true) + private String password; + + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) + private Cash cash; + + @Transient + private String confirmPassword; //실제 저장을 하지 않기 위해 @Transient 사용 + + @Enumerated(value = EnumType.STRING) + @Column(length = 20, nullable = false) + private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE; + + @ElementCollection(fetch = FetchType.EAGER) + private List roles = new ArrayList<>(); + + public enum MemberStatus { + MEMBER_ACTIVE("활동중"), + MEMBER_QUIT("탈퇴 상태"); + + @Getter + private String status; + + MemberStatus(String status) { + this.status = status; + } + } +// @Builder MemberMapper에서 PostDto의 confirmPassword 객체를 인식하지 못해 아래와 같이 변경 +// public Member(String name, String email, String password, List roles) { +// this.name = name; +// this.email = email; +// this.password = password; +// this.roles = roles; +// } + @Builder // Mapper에서 사용하도록 추가 + public Member(String name, String email, String password, List roles, String confirmPassword) { + this.name = name; + this.email = email; + this.password = password; + this.roles = roles; + this.confirmPassword = confirmPassword; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java new file mode 100644 index 00000000..312965f9 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/mapper/MemberMapper.java @@ -0,0 +1,20 @@ +package com.stockholm.main_project.member.mapper; + +import com.stockholm.main_project.member.dto.MemberPatchDto; +import com.stockholm.main_project.member.dto.MemberPostDto; +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface MemberMapper { + + + Member memberPostToMember(MemberPostDto requestBody); + Member memberPatchToMember(MemberPatchDto requestBody); + + @Mapping(source = "cash.cashId", target = "cash") + MemberResponseDto memberToMemberResponseDto(Member member); +} + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java new file mode 100644 index 00000000..3122c5e9 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package com.stockholm.main_project.member.repository; + +import com.stockholm.main_project.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); + Optional findByMemberId(Long memberId); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java b/server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java new file mode 100644 index 00000000..f669dbee --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/member/service/MemberService.java @@ -0,0 +1,124 @@ +package com.stockholm.main_project.member.service; + +import com.stockholm.main_project.auth.utils.CustomAuthorityUtils; +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import com.stockholm.main_project.stock.entity.StockOrder; +import com.stockholm.main_project.stock.repository.StockOrderRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@Slf4j +public class MemberService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final CustomAuthorityUtils authorityUtils; + private final StockOrderRepository stockOrderRepository; + + public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder, CustomAuthorityUtils authorityUtils, StockOrderRepository stockOrderRepository) { + this.memberRepository = memberRepository; + this.passwordEncoder = passwordEncoder; + this.authorityUtils = authorityUtils; + this.stockOrderRepository = stockOrderRepository; + } + + public Member createMember(Member member) { + + verifyExistsEmail(member.getEmail()); + + String password = member.getPassword(); + String confirmPassword = member.getConfirmPassword(); + + if (!password.equals(confirmPassword)) { + throw new BusinessLogicException(ExceptionCode.INVALID_PASSWORD); + }// 암호 재확인 기능 + + String encryptedPassword = passwordEncoder.encode(member.getPassword()); + member.setPassword(encryptedPassword); + + List roles = authorityUtils.createRoles(member.getEmail()); + member.setRoles(roles); + + member.setMemberStatus(Member.MemberStatus.MEMBER_ACTIVE); + Member saveMember = memberRepository.save(member); + System.out.println("# Create Member in DB"); + + return saveMember; + } + + public Member updateMember(Member member) { + + + Member findMember = findVerifiedMember(member.getMemberId()); + + Optional.ofNullable(member.getName()) + .ifPresent(name -> findMember.setName(name)); + + return memberRepository.save(findMember); + } + + public Member findMember(long memberId) { + + Member findMember = findVerifiedMember(memberId); + + if (findMember.getMemberId() != memberId) { + throw new BusinessLogicException(ExceptionCode.INVALID_FAILED); + } + + return findVerifiedMember(findMember.getMemberId()); + } + + public void deleteMember(long memberId) { + + memberRepository.deleteById(memberId); + + List orders = stockOrderRepository.findByMemberMemberId(memberId); + stockOrderRepository.deleteAll(orders); + } + + public Member findVerifiedMember(long memberId) { + + Optional optionalMember = + memberRepository.findByMemberId(memberId); + Member findMember = + optionalMember.orElseThrow(() -> + new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + return findMember; + } + + public void verifyExistsEmail(String email) { + Optional member = memberRepository.findByEmail(email); + if (member.isPresent()) + throw new BusinessLogicException(ExceptionCode.EMAIL_DUPLICATION); + } + + public Member findMemberByEmail(String email) { + Optional optionalUser = memberRepository.findByEmail(email); + + return optionalUser.orElse(null); + } + + public int findMemberIdByEmail(String email) { + Optional optionalMember = memberRepository.findByEmail(email); + + if (optionalMember.isPresent()) { + Member member = optionalMember.get(); + return (int) member.getMemberId(); + } else { + + throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND); + } + } + +// public void deleteStockOrdersByMemberId(Long memberId) { +// stockOrderRepository.deleteAllByMemberId(memberId); +// } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/scheduler/SchedulerConfig.java b/server/008main_project/src/main/java/com/stockholm/main_project/scheduler/SchedulerConfig.java new file mode 100644 index 00000000..5301d419 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/scheduler/SchedulerConfig.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.scheduler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +@Configuration +public class SchedulerConfig implements SchedulingConfigurer { + private final int POOL_SIZE = 10; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + final ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(POOL_SIZE); + threadPoolTaskScheduler.setThreadNamePrefix("test-scheduled-task-pool-"); + threadPoolTaskScheduler.initialize(); + + taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/scheduler/StockScheduler.java b/server/008main_project/src/main/java/com/stockholm/main_project/scheduler/StockScheduler.java new file mode 100644 index 00000000..b080e51f --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/scheduler/StockScheduler.java @@ -0,0 +1,248 @@ +package com.stockholm.main_project.scheduler; + +import com.stockholm.main_project.stock.service.*; +import com.stockholm.main_project.websocket.WebSocketController; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDateTime; + + +@Slf4j +@Service +public class StockScheduler { + private final StockAsBiService stockAsBiService; + private final StockMinService stockMinService; + private final CompanyService companyService; + private final TokenService tokenService; + private final StockOrderService stockOrderService; + private final WebSocketController webSocketController; + + public StockScheduler(StockAsBiService stockAsBiService, StockMinService stockMinService, CompanyService companyService, TokenService tokenService, StockOrderService stockOrderService, WebSocketController webSocketController) { + this.stockAsBiService = stockAsBiService; + this.stockMinService = stockMinService; + this.companyService = companyService; + this.tokenService = tokenService; + this.stockOrderService = stockOrderService; + this.webSocketController = webSocketController; + } + + @Scheduled(cron = "0 30 9-15 * * MON-FRI") + public void myScheduledStockAsBiMethod() throws InterruptedException { + // 이 메소드는 매주 월요일부터 금요일까지 9:30부터 15:30까지 30분 간격으로 실행됩니다. + // 원하는 작업을 여기에 추가하세요. + LocalDateTime start = LocalDateTime.now(); + stockAsBiService.updateStockAsBi(); + stockOrderService.checkOrder(); + LocalDateTime end = LocalDateTime.now(); + Duration duration = Duration.between(start, end); + System.out.println(duration.getSeconds()); + + } + + @Scheduled(cron = "0 0 10-15 * * MON-FRI") + public void myScheduledStockAsBiMethod2() throws InterruptedException { + // 이 메소드는 매주 월요일부터 금요일까지 9:30부터 15:30까지 30분 간격으로 실행됩니다. + // 원하는 작업을 여기에 추가하세요. + LocalDateTime start = LocalDateTime.now(); + stockAsBiService.updateStockAsBi(); + stockOrderService.checkOrder(); + LocalDateTime end = LocalDateTime.now(); + Duration duration = Duration.between(start, end); + System.out.println(duration.getSeconds()); + + } + + @Scheduled(cron = "0 30 9-15 * * MON-FRI") + public void myScheduledStockMinMethod() throws InterruptedException { + // 이 메소드는 매주 월요일부터 금요일까지 9:30부터 15:30까지 30분 간격으로 실행됩니다. + // 원하는 작업을 여기에 추가하세요. + LocalDateTime start = LocalDateTime.now(); + stockMinService.updateStockMin(); + LocalDateTime end = LocalDateTime.now(); + Duration duration = Duration.between(start, end); + System.out.println(duration.getSeconds()); + + } + + @Scheduled(cron = "0 0 10-15 * * MON-FRI") + public void myScheduledStockMinMethod2() throws InterruptedException { + // 이 메소드는 매주 월요일부터 금요일까지 9:30부터 15:30까지 30분 간격으로 실행됩니다. + // 원하는 작업을 여기에 추가하세요. + LocalDateTime start = LocalDateTime.now(); + stockMinService.updateStockMin(); + LocalDateTime end = LocalDateTime.now(); + Duration duration = Duration.between(start, end); + System.out.println(duration.getSeconds()); + + } + +// @Scheduled(fixedRate = 1000) +// public void run() throws InterruptedException { +// webSocketController.check(); +// } + +// @Scheduled(fixedRate = 10000000) +// public void secondSchedule() throws InterruptedException { +// LocalDateTime start = LocalDateTime.now(); +// stockAsBiService.updateStockAsBi(); +// stockMinService.updateStockMin(); +// LocalDateTime end = LocalDateTime.now(); +// Duration duration = Duration.between(start, end); +// System.out.println(duration.getSeconds()); +// +// } + + +// @Scheduled(fixedRate = 10000000) +// public void firstSchedule() throws InterruptedException { +// LocalDateTime start = LocalDateTime.now(); +// stockAsBiService.updateStockAsBi(); +// LocalDateTime end = LocalDateTime.now(); +// Duration duration = Duration.between(start, end); +// System.out.println(duration.getSeconds()); +// } + +// @Scheduled(fixedRate = 10000000) +// public void secondSchedule() throws InterruptedException { +// LocalDateTime start = LocalDateTime.now(); +// stockMinService.updateStockMin(); +// LocalDateTime end = LocalDateTime.now(); +// Duration duration = Duration.between(start, end); +// System.out.println(duration.getSeconds()); +// } + +} + + + +// @Scheduled(fixedRate = 10000000) +// public void secondSchedule() throws InterruptedException { +// LocalDateTime start = LocalDateTime.now(); +// stockMinService.updateStockMin(); +// LocalDateTime end = LocalDateTime.now(); +// Duration duration = Duration.between(start, end); +// System.out.println(duration.getSeconds()); +// } + +// @Scheduled(fixedRate = 10000000) +// public void secondSchedule() throws InterruptedException { +// LocalDateTime start = LocalDateTime.now(); +// stockAsBiService.updateStockAsBi(); +// stockMinService.updateStockMin(); +// LocalDateTime end = LocalDateTime.now(); +// Duration duration = Duration.between(start, end); +// System.out.println(duration.getSeconds()); +// } + + + + + +// @Scheduled(fixedRate = 60000) +// public void run() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run1"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } +// +// @Scheduled(fixedRate = 60000, initialDelay = 1000) +// public void run2() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run2"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } +// +// @Scheduled(fixedRate = 60000, initialDelay = 2000) +// public void run3() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run3"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } +// +// @Scheduled(fixedRate = 60000, initialDelay = 3000) +// public void run4() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run4"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } +// +// @Scheduled(fixedRate = 60000, initialDelay = 4000) +// public void run5() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run5"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } +// +// @Scheduled(fixedRate = 60000, initialDelay = 5000) +// public void run6() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run6"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } +// +// @Scheduled(fixedRate = 60000, initialDelay = 6000) +// public void run7() { +// List stockasbiDataDtos = stockService.getStockasbiData(); +// +// System.out.println("run7"); +// +// for(StockasbiDataDto stockasbiDataDto : stockasbiDataDtos) { +// log.info(stockasbiDataDto.getOutput1().getAskp1()); +// log.info(stockasbiDataDto.getOutput1().getAskp2()); +// log.info(stockasbiDataDto.getOutput1().getAskp3()); +// log.info(stockasbiDataDto.getOutput1().getAskp4()); +// log.info(stockasbiDataDto.getOutput1().getAskp5()); +// } +// } + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/CompanyController.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/CompanyController.java new file mode 100644 index 00000000..a76e6eb6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/CompanyController.java @@ -0,0 +1,92 @@ +package com.stockholm.main_project.stock.controller; + +import com.stockholm.main_project.stock.dto.CompanyResponseDto; +import com.stockholm.main_project.stock.dto.StockMinResponseDto; +import com.stockholm.main_project.stock.entity.Company; +import com.stockholm.main_project.stock.mapper.StockMapper; +import com.stockholm.main_project.stock.service.CompanyService; +import com.stockholm.main_project.stock.service.StockMinService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping("/companies") +@Valid +@Slf4j +public class CompanyController { + + private final CompanyService companyService; + private final StockMapper stockMapper; + private StockMinService stockMinService; + + public CompanyController(CompanyService companyService, StockMapper stockMapper, StockMinService stockMinService) { + this.companyService = companyService; + this.stockMapper = stockMapper; + this.stockMinService = stockMinService; + } + // swagger 추가 + @Operation(summary = "CompanyList 가져오기", description = "CompanyList를 Get해 옵니다", tags = { "Company" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = CompanyResponseDto.class)))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 전체 회사 리스트 + @GetMapping + public ResponseEntity getCompanyList() { + List companyList = companyService.findCompanies(); + List companyResponseDtoList = stockMapper.CompaniesToCompanyResponseDtos(companyList); + + return new ResponseEntity<>(companyResponseDtoList, HttpStatus.OK); + } + + // swagger 추가 + @Operation(summary = "특정 회사의 주식 호가 정보 가져오기", description = "특정 회사의 주식 호가 정보를 Get해 옵니다", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = CompanyResponseDto.class))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 주식 호가 정보 + @GetMapping("/{companyId}") + public ResponseEntity getCompanyStockAsBi(@PathVariable("companyId") Long comanyId) { + Company company = companyService.findCompanyById(comanyId); + CompanyResponseDto companyResponseDto = stockMapper.companyToCompanyResponseDto(company); + + return new ResponseEntity<>(companyResponseDto, HttpStatus.OK); + } + + @Operation(summary = "주식 한개 분봉 차트 불러오기", description = "주식 하나의 분봉 420개를 불러옵니다", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = StockMinResponseDto.class)))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 차트 하나 호출 + @GetMapping("/charts/{companyId}") + public ResponseEntity getCompanyChart(@PathVariable("companyId") long companyId) { + List stockMinList = stockMinService.getRecent420StockMin(companyId); + + return new ResponseEntity(stockMinList, HttpStatus.OK); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/LongPollingController.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/LongPollingController.java new file mode 100644 index 00000000..63737be0 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/LongPollingController.java @@ -0,0 +1,82 @@ +package com.stockholm.main_project.stock.controller; + +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.stock.dto.StockOrderResponseDto; +import com.stockholm.main_project.stock.entity.StockOrder; +import com.stockholm.main_project.stock.mapper.StockMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("long-polling") +public class LongPollingController { + private List updateBuyStockOrders; + private List updateSellStockOrders; + private final StockMapper stockMapper; + + public LongPollingController(StockMapper stockMapper) { + this.stockMapper = stockMapper; + } + + @Operation(summary = "예약된 매수, 매도가 완료되면 StockOrder를 반환한다", description = "완료된 StockOrder를 반환한다", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = StockOrderResponseDto.class))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + @GetMapping("/listen") + public ResponseEntity listenForUpdate(@AuthenticationPrincipal Member member) throws InterruptedException { + updateBuyStockOrders = new ArrayList<>(); + updateSellStockOrders = new ArrayList<>(); + + waitForStockOrdersToUpdate(); + + List memberBuyStockOrder = updateBuyStockOrders.stream() + .filter(stockOrder -> stockOrder.getMember().getMemberId() == member.getMemberId()) + .collect(Collectors.toList()); + + List memberSellStockOrder = updateSellStockOrders.stream() + .filter(stockOrder -> stockOrder.getMember().getMemberId() == member.getMemberId()) + .collect(Collectors.toList()); + + List buyStockOrderResponseDtos = stockMapper.stockOrdersToStockOrderResponseDtos(memberBuyStockOrder); + List sellStockOrderResponseDtos = stockMapper.stockOrdersToStockOrderResponseDtos(memberSellStockOrder); + + List> updateStockOrders = new ArrayList<>(); + updateStockOrders.add(buyStockOrderResponseDtos); + updateStockOrders.add(sellStockOrderResponseDtos); + + return new ResponseEntity(updateStockOrders, HttpStatus.OK); + } + + private void waitForStockOrdersToUpdate() throws InterruptedException { + // 변경된 데이터가 도착할 때까지 대기 + synchronized (this) { + if (updateBuyStockOrders.isEmpty() && updateSellStockOrders.isEmpty()) { + wait(); + } + } + } + + public synchronized void notifyDataUpdated(List buyStockOrders, + List sellStockOrders) { + updateBuyStockOrders = buyStockOrders; + updateSellStockOrders = sellStockOrders; + notify(); // 대기 중인 스레드를 깨워 응답을 보냄 + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StarController.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StarController.java new file mode 100644 index 00000000..667cbb0c --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StarController.java @@ -0,0 +1,44 @@ +package com.stockholm.main_project.stock.controller; + +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.stock.dto.StarResponseDto; +import com.stockholm.main_project.stock.service.StarService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/stars") +public class StarController { + private final StarService starService; + + public StarController(StarService starService) { + this.starService = starService; + } + + @PostMapping + public ResponseEntity setStar(@RequestParam long companyId, + @AuthenticationPrincipal Member member) { + starService.saveStar(member, companyId); + + return new ResponseEntity(HttpStatus.CREATED); + } + + @GetMapping + public ResponseEntity getStarList(@AuthenticationPrincipal Member member) { + List starResponseDtos = starService.getStarResponseDtoList(member); + + return new ResponseEntity<>(starResponseDtos, HttpStatus.OK); + } + + @DeleteMapping + public ResponseEntity deleteStar(@RequestParam long companyId, + @AuthenticationPrincipal Member member) { + starService.deleteStar(member, companyId); + + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StockController.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StockController.java new file mode 100644 index 00000000..926df199 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StockController.java @@ -0,0 +1,43 @@ +package com.stockholm.main_project.stock.controller; + + +import com.stockholm.main_project.stock.dto.StockMinResponseDto; +import com.stockholm.main_project.stock.dto.StockasbiDataDto; +import com.stockholm.main_project.stock.service.ApiCallService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class StockController { + + private final ApiCallService apiCallService; + + public StockController(ApiCallService apiCallService) { + this.apiCallService = apiCallService; + } + + + @Operation(summary = "코스피 월봉 정보 가져오기", description = "월 초부터 현재 달까지의 월 코스피 정보를 불러옵니다.", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + @GetMapping("/kospi") + public ResponseEntity getKospiMonth() { + String kospi = apiCallService.getKospiMonthFromApi(); + return ResponseEntity.ok(kospi); + } + + +} + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StockOrderController.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StockOrderController.java new file mode 100644 index 00000000..d63d516c --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/controller/StockOrderController.java @@ -0,0 +1,141 @@ +package com.stockholm.main_project.stock.controller; + +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.stock.dto.CompanyResponseDto; +import com.stockholm.main_project.stock.dto.StockHoldResponseDto; +import com.stockholm.main_project.stock.dto.StockOrderResponseDto; +import com.stockholm.main_project.stock.entity.StockOrder; +import com.stockholm.main_project.stock.mapper.StockMapper; +import com.stockholm.main_project.stock.service.StockHoldService; +import com.stockholm.main_project.stock.service.StockOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/stock") +public class StockOrderController { + + private final StockOrderService stockOrderService; + private final StockMapper stockMapper; + private final StockHoldService stockHoldService; + + public StockOrderController(StockOrderService stockOrderService, StockMapper stockMapper, StockHoldService stockHoldService) { + this.stockOrderService = stockOrderService; + this.stockMapper = stockMapper; + this.stockHoldService = stockHoldService; + } + + @Operation(summary = "주식을 매수 한다", description = "회사 주식을 매수하는 api", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = StockOrderResponseDto.class))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 매수 api + @PostMapping("/buy") + public ResponseEntity buyStocks(@RequestParam(name = "companyId") long companyId, + @RequestParam(name = "price") long price, + @RequestParam(name = "stockCount") int stockCount, + @AuthenticationPrincipal Member member) { + StockOrder stockOrder = stockOrderService.buyStocks(member, companyId, price, stockCount); + StockOrderResponseDto stockOrderResponseDto = stockMapper.stockOrderToStockOrderResponseDto(stockOrder); + + return new ResponseEntity<>(stockOrderResponseDto, HttpStatus.CREATED); + } + + @Operation(summary = "주식을 매도 한다", description = "회사 주식을 매도하는 api", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = StockOrderResponseDto.class))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 매도 api + @PostMapping("/sell") + public ResponseEntity sellStocks(@RequestParam(name = "companyId") long companyId, + @RequestParam(name = "price") long price, + @RequestParam(name = "stockCount") int stockCount, + @AuthenticationPrincipal Member member) { + StockOrder stockOrder = stockOrderService.sellStocks(member, companyId, price, stockCount); + StockOrderResponseDto stockOrderResponseDto = stockMapper.stockOrderToStockOrderResponseDto(stockOrder); + + return new ResponseEntity<>(stockOrderResponseDto, HttpStatus.CREATED); + } + + @Operation(summary = "멤버의 stockHold를 반환한다", description = "멤버의 stockHold를 반환하는 api", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = StockHoldResponseDto.class)))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 보유 주식 정보들 반환하는 api + @GetMapping("/stockholds") + public ResponseEntity getStockHolds(@AuthenticationPrincipal Member member) { + List stockHoldResponseDtos = stockHoldService.findStockHolds(member.getMemberId()); + //vList stockHoldResponseDtos = companyMapper.stockHoldToStockHoldResponseDto(stockHoldList); + stockHoldResponseDtos = stockHoldService.setPercentage(stockHoldResponseDtos); + + return new ResponseEntity<>(stockHoldResponseDtos, HttpStatus.OK); + } + + @Operation(summary = "멤버의 stockOrder를 반환한다", description = "멤버의 stockOrder를 반환하는 api", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = StockOrderResponseDto.class)))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 멤버의 stockOrder를 반환하는 api + @GetMapping("/stockorders") + public ResponseEntity getStockOrders(@AuthenticationPrincipal Member member) { + List stockOrderResponseDtos = stockOrderService.getMemberStockOrders(member.getMemberId()); + + return new ResponseEntity<>(stockOrderResponseDtos, HttpStatus.OK); + } + + @Operation(summary = "미체결된 매수, 매도 StockOrder 삭제", description = "미 체결된 매수, 매도 삭제하는 api", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + // 미 체결된 매수, 매도 삭제하는 api + @DeleteMapping("/stockorders") + public void deleteStockOrders(@AuthenticationPrincipal Member member, + @RequestParam("stockOrderId") long stockOrderId, + @RequestParam("stockCount") int stockCount) { + stockOrderService.deleteStockOrder(member, stockOrderId, stockCount); + } + + @Operation(summary = "예약 매도 매수 기능 실행", description = "30분 마다 실행되는 예약 매도 매수기능 실행하는 api", tags = { "Stock" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + @GetMapping("checkOrder") + public ResponseEntity checkOrder() { + stockOrderService.checkOrder(); + + return new ResponseEntity(HttpStatus.OK); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/CompanyResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/CompanyResponseDto.java new file mode 100644 index 00000000..505d67ee --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/CompanyResponseDto.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.stock.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CompanyResponseDto { + @Schema(description = "회사 Id", defaultValue = "데이터 베이스 Id") + private long companyId; + @Schema(description = "회사의 고유 주식 코드", defaultValue = "6자리로 이루어진 회사의 고유 주식 코드 (ex. 005930)") + private String code; + @Schema(description = "회사 한글 이름", defaultValue = "회사 한글 이름 (ex. 삼성전자)") + private String korName; + @Schema(description = "주식 매수/매도 호가 가격, 수량", defaultValue = "as = 매도 호가, bi = 매수 호가") + private StockAsBiResponseDto stockAsBiResponseDto; + @Schema(description = "주식 정보", defaultValue = "prpr = 주식 현재가, vrss = 전일 대비 상승, 하락률, ctcr = 누적 거래량, vol = 누적 거래량, pbmn = 누적 거래 대금") + private StockInfResponseDto stockInfResponseDto; +} + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StarResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StarResponseDto.java new file mode 100644 index 00000000..a56a832d --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StarResponseDto.java @@ -0,0 +1,21 @@ +package com.stockholm.main_project.stock.dto; + +import com.stockholm.main_project.member.dto.MemberResponseDto; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.stock.entity.Company; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Getter +@Setter +public class StarResponseDto { + private long starId; + + private long memberId; + + private CompanyResponseDto companyResponseDto; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockAsBiResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockAsBiResponseDto.java new file mode 100644 index 00000000..bebc0048 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockAsBiResponseDto.java @@ -0,0 +1,61 @@ +package com.stockholm.main_project.stock.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StockAsBiResponseDto { + + private long stockAsBiId; + + private long companyId; + + //매도 호가 + private String askp1; + private String askp2; + private String askp3; + private String askp4; + private String askp5; + private String askp6; + private String askp7; + private String askp8; + private String askp9; + private String askp10; + + //매도 잔량 + private String askp_rsqn1; + private String askp_rsqn2; + private String askp_rsqn3; + private String askp_rsqn4; + private String askp_rsqn5; + private String askp_rsqn6; + private String askp_rsqn7; + private String askp_rsqn8; + private String askp_rsqn9; + private String askp_rsqn10; + + //매수 호가 + private String bidp1; + private String bidp2; + private String bidp3; + private String bidp4; + private String bidp5; + private String bidp6; + private String bidp7; + private String bidp8; + private String bidp9; + private String bidp10; + + //매수 잔량 + private String bidp_rsqn1; + private String bidp_rsqn2; + private String bidp_rsqn3; + private String bidp_rsqn4; + private String bidp_rsqn5; + private String bidp_rsqn6; + private String bidp_rsqn7; + private String bidp_rsqn8; + private String bidp_rsqn9; + private String bidp_rsqn10; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockHoldResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockHoldResponseDto.java new file mode 100644 index 00000000..0c9050b0 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockHoldResponseDto.java @@ -0,0 +1,26 @@ +package com.stockholm.main_project.stock.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StockHoldResponseDto { + private long stockHoldId; + + private long memberId; + + private long companyId; + + private String companyKorName; + + private int stockCount; + + private long totalPrice; + + private double percentage; + + private long stockReturn; + + private long reserveSellStockCount; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockInfResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockInfResponseDto.java new file mode 100644 index 00000000..f65927d3 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockInfResponseDto.java @@ -0,0 +1,25 @@ +package com.stockholm.main_project.stock.dto; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; + +@Getter +@Setter +public class StockInfResponseDto { + private long stockInfId; + + private long companyId; + + //주식 현재가 + private String stck_prpr; + //전일 대비 + private String prdy_vrss; + //전일 대비율 + private String prdy_ctrt; + //누적 거래량 + private String acml_vol; + //누적 거래대금 + private String acml_tr_pbmn; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockMinDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockMinDto.java new file mode 100644 index 00000000..abd8e924 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockMinDto.java @@ -0,0 +1,51 @@ +package com.stockholm.main_project.stock.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class StockMinDto { + private StockMinOutput1 output1; + private List output2; + + @Getter + @Setter + @NoArgsConstructor + public class StockMinOutput1 { + //한글 종목 명 + private String hts_kor_isnm; + //주식 현재가 + private String stck_prpr; + //전일 대비 + private String prdy_vrss; + //전일 대비율 + private String prdy_ctrt; + //누적 거래량 + private String acml_vol; + //누적 거래대금 + private String acml_tr_pbmn; + } + + @Getter + @Setter + @NoArgsConstructor + public static class StockMinOutput2 { + //주식 체결 시간 + private String stck_cntg_hour; + //주식 현재가 + private String stck_prpr; + //주식 시가 + private String stck_oprc; + //주식 최고가 + private String stck_hgpr; + //주식 최저가 + private String stck_lwpr; + //체결 거래량 + private String cntg_vol; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockMinResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockMinResponseDto.java new file mode 100644 index 00000000..5601dced --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockMinResponseDto.java @@ -0,0 +1,36 @@ +package com.stockholm.main_project.stock.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class StockMinResponseDto { + @Schema(description = "주식 분봉 아이디", defaultValue = "1") + private long stockMinId; + @Schema(description = "주식 회사 아이디", defaultValue = "1") + private long companyId; + @Schema(description = "LocalDateTime 시간", defaultValue = "2023-09-04 10:01:00") + private LocalDateTime stockTradeTime; + + //주식 체결 시간(문자열) + @Schema(description = "HHMMSS 시간", defaultValue = "100100") + private String stck_cntg_hour; + //주식 현재가 + @Schema(description = "주식 현재가", defaultValue = "7600") + private String stck_prpr; + //주식 시가 + @Schema(description = "주식 시가", defaultValue = "7400") + private String stck_oprc; + //주식 최고가 + @Schema(description = "주식 최고가", defaultValue = "8200") + private String stck_hgpr; + //주식 최저가 + @Schema(description = "주식 최저가", defaultValue = "7200") + private String stck_lwpr; + @Schema(description = "체결 거래량", defaultValue = "32000") + private String cntg_vol; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockOrderResponseDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockOrderResponseDto.java new file mode 100644 index 00000000..2b3722d6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockOrderResponseDto.java @@ -0,0 +1,27 @@ +package com.stockholm.main_project.stock.dto; + +import com.stockholm.main_project.stock.entity.StockOrder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class StockOrderResponseDto { + private long stockOrderId; + + private int stockCount; + + private long memberId; + + private long companyId; + + private StockOrder.OrderTypes OrderTypes; + + private com.stockholm.main_project.stock.entity.StockOrder.OrderStates OrderStates; + + private long price; + + private LocalDateTime modifiedAt; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockasbiDataDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockasbiDataDto.java new file mode 100644 index 00000000..da44f428 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/dto/StockasbiDataDto.java @@ -0,0 +1,87 @@ +package com.stockholm.main_project.stock.dto; + +import lombok.*; + + +@Data +@NoArgsConstructor +public class StockasbiDataDto { + private StockAsBiOutput1 output1; + //private StockAsBiOutput2 output2; + + @Getter + @Setter + @NoArgsConstructor + public class StockAsBiOutput1 { + + // 호가 접수 시간 + private String aspr_acpt_hour; + + //매도 호가 + private String askp1; + private String askp2; + private String askp3; + private String askp4; + private String askp5; + private String askp6; + private String askp7; + private String askp8; + private String askp9; + private String askp10; + + //매도 잔량 + private String askp_rsqn1; + private String askp_rsqn2; + private String askp_rsqn3; + private String askp_rsqn4; + private String askp_rsqn5; + private String askp_rsqn6; + private String askp_rsqn7; + private String askp_rsqn8; + private String askp_rsqn9; + private String askp_rsqn10; + + //매수 호가 + private String bidp1; + private String bidp2; + private String bidp3; + private String bidp4; + private String bidp5; + private String bidp6; + private String bidp7; + private String bidp8; + private String bidp9; + private String bidp10; + + //매수 잔량 + private String bidp_rsqn1; + private String bidp_rsqn2; + private String bidp_rsqn3; + private String bidp_rsqn4; + private String bidp_rsqn5; + private String bidp_rsqn6; + private String bidp_rsqn7; + private String bidp_rsqn8; + private String bidp_rsqn9; + private String bidp_rsqn10; + + } + +// @Getter +// @Setter +// @NoArgsConstructor +// public class StockAsBiOutput2 { +// +// //주식 현재가 +// private String stck_prpr; +// //주식 시가 +// private String stck_oprc; +// //주식 최고가 +// private String stck_hgpr; +// //주식 최저가 +// private String stck_lwpr; +// //주식 기준가 +// private String stck_sdpr; +// } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Company.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Company.java new file mode 100644 index 00000000..68d9a4d6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Company.java @@ -0,0 +1,31 @@ +package com.stockholm.main_project.stock.entity; + +import com.stockholm.main_project.audit.Auditable; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Company extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long companyId; + + @Column + private String code; + + @Column + private String korName; + + @OneToOne(mappedBy = "company", cascade = CascadeType.ALL) + private StockAsBi stockAsBi; + + @OneToOne(mappedBy = "company", cascade = CascadeType.ALL) + private StockInf stockInf; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Star.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Star.java new file mode 100644 index 00000000..12ea5421 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Star.java @@ -0,0 +1,30 @@ +package com.stockholm.main_project.stock.entity; + +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Star extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long starId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "MEMBER_ID") + @OnDelete(action = OnDeleteAction.CASCADE) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "COMPANY_ID") + private Company company; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockAsBi.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockAsBi.java new file mode 100644 index 00000000..7d50008d --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockAsBi.java @@ -0,0 +1,110 @@ +package com.stockholm.main_project.stock.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class StockAsBi { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long stockAsBiId; + + @OneToOne + @JoinColumn(name = "COMPANY_ID") + private Company company; + + //매도 호가 + @Column + private String askp1; + @Column + private String askp2; + @Column + private String askp3; + @Column + private String askp4; + @Column + private String askp5; + @Column + private String askp6; + @Column + private String askp7; + @Column + private String askp8; + @Column + private String askp9; + @Column + private String askp10; + + //매도 잔량 + @Column + private String askp_rsqn1; + @Column + private String askp_rsqn2; + @Column + private String askp_rsqn3; + @Column + private String askp_rsqn4; + @Column + private String askp_rsqn5; + @Column + private String askp_rsqn6; + @Column + private String askp_rsqn7; + @Column + private String askp_rsqn8; + @Column + private String askp_rsqn9; + @Column + private String askp_rsqn10; + + //매수 호가 + @Column + private String bidp1; + @Column + private String bidp2; + @Column + private String bidp3; + @Column + private String bidp4; + @Column + private String bidp5; + @Column + private String bidp6; + @Column + private String bidp7; + @Column + private String bidp8; + @Column + private String bidp9; + @Column + private String bidp10; + + //매수 잔량 + @Column + private String bidp_rsqn1; + @Column + private String bidp_rsqn2; + @Column + private String bidp_rsqn3; + @Column + private String bidp_rsqn4; + @Column + private String bidp_rsqn5; + @Column + private String bidp_rsqn6; + @Column + private String bidp_rsqn7; + @Column + private String bidp_rsqn8; + @Column + private String bidp_rsqn9; + @Column + private String bidp_rsqn10; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockHold.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockHold.java new file mode 100644 index 00000000..182dd559 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockHold.java @@ -0,0 +1,40 @@ +package com.stockholm.main_project.stock.entity; + +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class StockHold extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long stockHoldId; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + @OnDelete(action = OnDeleteAction.CASCADE) + private Member member; + + @ManyToOne + @JoinColumn(name = "COMPANY_ID") + private Company company; + + @Column + private int stockCount; + + @Column + private int reserveStockCount; + + @Column + private long price; + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockInf.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockInf.java new file mode 100644 index 00000000..0de5ecbf --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockInf.java @@ -0,0 +1,37 @@ +package com.stockholm.main_project.stock.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class StockInf { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long stockInfId; + + @OneToOne + @JoinColumn(name = "COMPANY_ID") + private Company company; + + //주식 현재가 + @Column + private String stck_prpr; + //전일 대비 + @Column + private String prdy_vrss; + //전일 대비율 + @Column + private String prdy_ctrt; + //누적 거래량 + @Column + private String acml_vol; + //누적 거래대금 + @Column + private String acml_tr_pbmn; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockMin.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockMin.java new file mode 100644 index 00000000..ae2393bf --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockMin.java @@ -0,0 +1,63 @@ +package com.stockholm.main_project.stock.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class StockMin { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long stockMinId; + + @ManyToOne + @JoinColumn(name = "COMPANY_ID") + private Company company; + + @Column + private LocalDateTime stockTradeTime; + + //주식 체결 시간(문자열) + @Column + private String stck_cntg_hour; + //주식 현재가 + @Column + private String stck_prpr; + //주식 시가 + @Column + private String stck_oprc; + @Column + //주식 최고가 + private String stck_hgpr; + @Column + //주식 최저가 + private String stck_lwpr; + @Column + //체결 거래량 + private String cntg_vol; + public void setTradeTime(LocalDateTime now) { + + // 문자열에서 시, 분, 초 추출 + int hour = Integer.parseInt(this.stck_cntg_hour.substring(0, 2)); + int minute = Integer.parseInt(this.stck_cntg_hour.substring(2, 4)); + int second = Integer.parseInt(this.stck_cntg_hour.substring(4, 6)); + + // LocalDateTime 객체 생성 + LocalDateTime customDateTime = LocalDateTime.of( + now.getYear(), + now.getMonth(), + now.getDayOfMonth(), + hour, + minute, + second + ); + + this.stockTradeTime = customDateTime; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockOrder.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockOrder.java new file mode 100644 index 00000000..513355b6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/StockOrder.java @@ -0,0 +1,67 @@ +package com.stockholm.main_project.stock.entity; + +import com.stockholm.main_project.audit.Auditable; +import com.stockholm.main_project.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class StockOrder extends Auditable{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long stockOrderId; + + @Column + private int stockCount; + + @ManyToOne() + @JoinColumn(name = "MEMBER_ID") + @OnDelete(action = OnDeleteAction.CASCADE) + private Member member; + + @ManyToOne() + @JoinColumn(name = "COMPANY_ID") + private Company company; + + @Enumerated(value = EnumType.STRING) + @Column(length = 20, nullable = false) + private OrderTypes orderTypes; + + @Enumerated(value = EnumType.STRING) + @Column(length = 20, nullable = false) + private OrderStates orderStates; + + @Column + private long price; + + public enum OrderTypes { + SELL("매도"), + BUY("매수"); + @Getter + @Setter + private String types; + + OrderTypes(String types) { this.types = types; } + } + + public enum OrderStates { + ORDER_COMPLETE("채결 완료"), + ORDER_WAIT("체결 대기"); + @Getter + @Setter + private String states; + + OrderStates(String status) { + this.states = status; + } + } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Token.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Token.java new file mode 100644 index 00000000..a95c042d --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/entity/Token.java @@ -0,0 +1,25 @@ +package com.stockholm.main_project.stock.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Token { + @Id + long tokenId; + + @Column(length = 500) + private String token; + + @Column + private LocalDateTime expired; + + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/mapper/ApiMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/mapper/ApiMapper.java new file mode 100644 index 00000000..c243cd22 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/mapper/ApiMapper.java @@ -0,0 +1,16 @@ +package com.stockholm.main_project.stock.mapper; + +import com.stockholm.main_project.stock.dto.StockMinDto; +import com.stockholm.main_project.stock.dto.StockasbiDataDto; +import com.stockholm.main_project.stock.entity.StockAsBi; +import com.stockholm.main_project.stock.entity.StockInf; +import com.stockholm.main_project.stock.entity.StockMin; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface ApiMapper { + StockAsBi stockAsBiOutput1ToStockAsBi(StockasbiDataDto.StockAsBiOutput1 stock); + StockMin stockMinOutput2ToStockMin(StockMinDto.StockMinOutput2 stock); + StockInf stockMinOutput1ToStockInf(StockMinDto.StockMinOutput1 stock); + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/mapper/StockMapper.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/mapper/StockMapper.java new file mode 100644 index 00000000..804b37d7 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/mapper/StockMapper.java @@ -0,0 +1,91 @@ +package com.stockholm.main_project.stock.mapper; + +import com.stockholm.main_project.stock.dto.*; +import com.stockholm.main_project.stock.entity.*; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.ArrayList; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface StockMapper { + default List CompaniesToCompanyResponseDtos(List companyList) { + + List companyResponseDtoList = new ArrayList<>(); + + for(Company company : companyList) { + CompanyResponseDto companyResponseDto = companyToCompanyResponseDto(company); + companyResponseDtoList.add(companyResponseDto); + } + return companyResponseDtoList; + } + + default CompanyResponseDto companyToCompanyResponseDto(Company company) { + CompanyResponseDto companyResponseDto = new CompanyResponseDto(); + + companyResponseDto.setCompanyId(company.getCompanyId()); + companyResponseDto.setCode(company.getCode()); + companyResponseDto.setKorName(company.getKorName()); + companyResponseDto.setStockAsBiResponseDto(stockAsBiToStockAsBiResponseDto(company.getStockAsBi())); + companyResponseDto.setStockInfResponseDto(stockInfToStockInfResponseDto(company.getStockInf())); + + return companyResponseDto; + }; + + @Mapping(source = "company.companyId", target = "companyId") + StockInfResponseDto stockInfToStockInfResponseDto(StockInf stockInf); + @Mapping(source = "company.companyId", target = "companyId") + StockAsBiResponseDto stockAsBiToStockAsBiResponseDto(StockAsBi stockAsBi); + @Mapping(source = "company.companyId", target = "companyId") + StockMinResponseDto stockMinToStockMinResponseDto(StockMin stockMin); + @Mapping(source = "company.companyId", target = "companyId") + @Mapping(source = "member.memberId", target = "memberId") + StockOrderResponseDto stockOrderToStockOrderResponseDto(StockOrder stockOrder); + default List stockOrdersToStockOrderResponseDtos(List stockOrders) { + List stockOrderResponseDtos = new ArrayList<>(); + + for(StockOrder stockOrder : stockOrders) { + StockOrderResponseDto stockOrderResponseDto = stockOrderToStockOrderResponseDto(stockOrder); + stockOrderResponseDtos.add(stockOrderResponseDto); + } + + return stockOrderResponseDtos; + } + default List stockHoldToStockHoldResponseDto(List stockHolds) { + List stockHoldResponseDtos = new ArrayList<>(); + + for(StockHold stockHold : stockHolds) { + StockHoldResponseDto stockHoldResponseDto = new StockHoldResponseDto(); + + stockHoldResponseDto.setStockHoldId(stockHold.getStockHoldId()); + stockHoldResponseDto.setCompanyId(stockHold.getCompany().getCompanyId()); + stockHoldResponseDto.setCompanyKorName(stockHold.getCompany().getKorName()); + stockHoldResponseDto.setMemberId(stockHold.getMember().getMemberId()); + stockHoldResponseDto.setStockCount(stockHold.getStockCount()); + stockHoldResponseDto.setReserveSellStockCount(stockHold.getReserveStockCount()); + stockHoldResponseDto.setTotalPrice(stockHold.getPrice()); + stockHoldResponseDto.setPercentage(0D); + stockHoldResponseDto.setStockReturn(0); + + + stockHoldResponseDtos.add(stockHoldResponseDto); + } + return stockHoldResponseDtos; + } + + default List starsToStarResponseDtos(List stars) { + List starResponseDtos = new ArrayList<>(); + + for(Star star : stars) { + StarResponseDto starResponseDto = new StarResponseDto(); + + starResponseDto.setStarId(star.getStarId()); + starResponseDto.setMemberId(star.getMember().getMemberId()); + starResponseDto.setCompanyResponseDto(companyToCompanyResponseDto(star.getCompany())); + + starResponseDtos.add(starResponseDto); + } + return starResponseDtos; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/CompanyRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/CompanyRepository.java new file mode 100644 index 00000000..ace6e976 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/CompanyRepository.java @@ -0,0 +1,19 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.Company; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface CompanyRepository extends JpaRepository { + + @Query("SELECT c FROM Company c JOIN FETCH c.stockAsBi JOIN FETCH c.stockInf WHERE c.code = :code") + Company findByCode(@Param("code") String code); + @Query("SELECT c FROM Company c JOIN FETCH c.stockAsBi JOIN FETCH c.stockInf WHERE c.companyId = :companyId") + Company findByCompanyId(@Param("companyId") long companyId); + @Query("SELECT c from Company c join fetch c.stockAsBi JOIN FETCH c.stockInf") + List findAll(); + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StarRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StarRepository.java new file mode 100644 index 00000000..5d1c8bac --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StarRepository.java @@ -0,0 +1,14 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.Star; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface StarRepository extends JpaRepository { + @Query("SELECT s FROM Star s JOIN FETCH s.member JOIN FETCH s.company WHERE s.member.memberId = :memberId") + List findAllByMember_MemberId(long memberId); + + Star findByMember_MemberIdAndCompanyCompanyId(long memberId, long companyId); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockAsBiRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockAsBiRepository.java new file mode 100644 index 00000000..17a53a0a --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockAsBiRepository.java @@ -0,0 +1,8 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.StockAsBi; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StockAsBiRepository extends JpaRepository { +} + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockHoldRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockHoldRepository.java new file mode 100644 index 00000000..decf8d92 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockHoldRepository.java @@ -0,0 +1,12 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.StockHold; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface StockHoldRepository extends JpaRepository { + StockHold findByCompanyCompanyIdAndMemberMemberId(long companyId, long memberId); + List findAllByMember_MemberId(long memberId); + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockMinRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockMinRepository.java new file mode 100644 index 00000000..f435ab62 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockMinRepository.java @@ -0,0 +1,13 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.StockMin; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface StockMinRepository extends JpaRepository { + List findAllByCompanyCompanyId(long companyId); + @Query(value = "SELECT * FROM stock_min s WHERE s.company_id = ?1 ORDER BY s.stock_min_id DESC LIMIT 420", nativeQuery = true) + List findTop420ByCompanyIdOrderByStockMinIdDesc(long companyId); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockOrderRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockOrderRepository.java new file mode 100644 index 00000000..b4a17578 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/StockOrderRepository.java @@ -0,0 +1,19 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.StockOrder; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface StockOrderRepository extends JpaRepository { + List findAllByCompanyCompanyIdAndOrderStates(long company_companyId, StockOrder.OrderStates orderStates); + List findAllByMember_MemberId(long memberId); + List findAllByMember_MemberIdOrderByModifiedAtDesc(long memberId); + List findAllByMember_MemberIdAndCompany_CompanyIdAndOrderStatesAndOrderTypes(long memberId, long companyId, StockOrder.OrderStates orderStates, StockOrder.OrderTypes orderTypes); + + List findByMemberMemberId(long memberId); + // MEMBER_ID로 주식 주문을 모두 삭제하는 JPQL 쿼리 +// @Modifying +// @Query("DELETE FROM StockOrder so WHERE so.memberId = :memberId") +// void deleteAllByMemberId(@Param("memberId") Long memberId); +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/TokenRepository.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/TokenRepository.java new file mode 100644 index 00000000..9f071ef6 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/repository/TokenRepository.java @@ -0,0 +1,7 @@ +package com.stockholm.main_project.stock.repository; + +import com.stockholm.main_project.stock.entity.Token; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TokenRepository extends JpaRepository { +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/ApiCallService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/ApiCallService.java new file mode 100644 index 00000000..5e165814 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/ApiCallService.java @@ -0,0 +1,145 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.stock.dto.StockasbiDataDto; +import com.stockholm.main_project.stock.dto.StockMinDto; +import com.stockholm.main_project.stock.repository.CompanyRepository; +import com.stockholm.main_project.utils.Time; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; + +import javax.transaction.Transactional; +import java.time.LocalDateTime; + + +@Service +@Transactional +@Slf4j +public class ApiCallService { + @Getter + @Value("${token.app-key}") + private String APP_KEY; + + @Getter + @Value("${token.app-secret}") + private String APP_SECRET; + + @Getter + @Value("${stock-url.token}") + private String TOKEN_URL; + + @Getter + @Value("${stock-url.stockasbi}") + private String STOCKASBI_URL; + + @Getter + @Value("${stock-url.stockhour}") + private String STOCKHOUR_URL; + + @Getter + @Value("${stock-url.kospi}") + private String KOSPI_URL; + + + private final String FID_ETC_CLS_CODE = ""; + private final String FID_COND_MRKT_DIV_CODE = "J"; + // private final String FID_INPUT_HOUR_1 = "153000"; + private final String FID_PW_DATA_INCU_YN = "Y"; + + private RestTemplate restTemplate = new RestTemplate(); + + public ApiCallService(TokenService tokenService, CompanyRepository companyRepository) { + this.tokenService = tokenService; + this.companyRepository = companyRepository; + } + + private final TokenService tokenService; + + private final CompanyRepository companyRepository; + + public StockasbiDataDto getStockasbiDataFromApi(String stockCode){ + String token = tokenService.getAccessToken(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + token); + headers.add("appkey", APP_KEY); + headers.add("appsecret", APP_SECRET); + headers.add("tr_id", "FHKST01010200"); + + String uri = STOCKASBI_URL + "?FID_COND_MRKT_DIV_CODE=J&FID_INPUT_ISCD=" + stockCode; + + HttpEntity entity = new HttpEntity<>("parameters", headers); + + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, entity, new ParameterizedTypeReference() {}); + + if (response.getStatusCode().is2xxSuccessful()) { + StockasbiDataDto stockasbiDataDto = response.getBody(); + return stockasbiDataDto; + } else { + log.info("error"); + return null; + } + + } + + public StockMinDto getStockMinDataFromApi(String stockCode, String strHour) { + String token = tokenService.getAccessToken(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + token); + headers.add("appkey", APP_KEY); + headers.add("appsecret", APP_SECRET); + headers.add("tr_id", "FHKST03010200"); + + String uri = STOCKHOUR_URL + "?FID_COND_MRKT_DIV_CODE=" + FID_COND_MRKT_DIV_CODE + "&FID_INPUT_ISCD=" + stockCode + "&FID_ETC_CLS_CODE=" + FID_ETC_CLS_CODE + + "&FID_INPUT_HOUR_1=" + strHour + "&FID_PW_DATA_INCU_YN=" + FID_PW_DATA_INCU_YN; + + HttpEntity entity = new HttpEntity<>("parameters", headers); + + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, entity, new ParameterizedTypeReference() {}); + + if (response.getStatusCode().is2xxSuccessful()) { + StockMinDto stockMinDto = response.getBody(); + return stockMinDto; + + } else { + log.info("error"); + return null; + } + + } + + public String getKospiMonthFromApi(){ + String token = tokenService.getAccessToken(); + + LocalDateTime localDateTime = LocalDateTime.now(); + + String strMonth = Time.strMonth(localDateTime); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + token); + headers.add("appkey", APP_KEY); + headers.add("appsecret", APP_SECRET); + headers.add("tr_id", "FHKUP03500100"); + + String uri = KOSPI_URL + "?FID_COND_MRKT_DIV_CODE=U&FID_INPUT_ISCD=" + "0001" + "&FID_INPUT_DATE_1=" + "20230101" + +"&FID_INPUT_DATE_2=" + strMonth + "&FID_PERIOD_DIV_CODE=" + "M"; + + HttpEntity entity = new HttpEntity<>("parameters", headers); + + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, entity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + return response.getBody(); + } else { + log.info("error"); + return null; + } + + } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/CompanyService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/CompanyService.java new file mode 100644 index 00000000..f506e914 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/CompanyService.java @@ -0,0 +1,85 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.stock.dto.StockasbiDataDto; +import com.stockholm.main_project.stock.entity.Company; +import com.stockholm.main_project.stock.entity.StockAsBi; +import com.stockholm.main_project.stock.mapper.ApiMapper; +import com.stockholm.main_project.stock.repository.CompanyRepository; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@Transactional +public class CompanyService { + private final CompanyRepository companyRepository; + private final ApiCallService apiCallService; + private final ApiMapper apiMapper; + + public CompanyService(CompanyRepository companyRepository, ApiCallService apiCallService, ApiMapper apiMapper) { + this.companyRepository = companyRepository; + this.apiCallService = apiCallService; + this.apiMapper = apiMapper; + } + + // 특정 회사 리턴 + public Company findCompanyByCode(String stockCode) { + Company company = companyRepository.findByCode(stockCode); + return company; + } + + public Company findCompanyById(long companyId) { + Company company = companyRepository.findByCompanyId(companyId); + return company; + } + + // 모든 회사 리턴 + public List findCompanies() { + List companies = companyRepository.findAll(); + + return companies; + } + + + // 특정 회사 저장 + public Company saveCompany(Company company) { + return companyRepository.save(company); + } + + // 모든 회사 저장 + public List saveCompanies(List companies) { + return companyRepository.saveAll(companies); + } + + public void fillCompany() { + Company company = new Company(); + + } + + public void fillCompaines() throws InterruptedException { + List korName = List.of("삼성전자", "POSCO홀딩스", "셀트리온", "에코프로", "에코프로비엠", "디와이", "쿠쿠홀딩스", "카카오뱅크", "한세엠케이", "KG케미칼", "LG화학", "현대차", "LG전자", "기아"); + List code = List.of("005930", "005490", "068270", "086520", "247540", "013570", "192400", "323410", "069640", "001390", "051910", "005380", "066570", "000270"); + + for(int i = 0; i < code.size(); i++) { + Company company = new Company(); + company.setCode(code.get(i)); + company.setKorName(korName.get(i)); + company.setStockAsBi(new StockAsBi()); + + StockasbiDataDto stockasbiDataDto = apiCallService.getStockasbiDataFromApi(company.getCode()); + // mapper로 정리 된 값 받기 + StockAsBi stockAsBi = apiMapper.stockAsBiOutput1ToStockAsBi(stockasbiDataDto.getOutput1()); + + company.setStockAsBi(stockAsBi); + stockAsBi.setCompany(company); + + Thread.sleep(500); + + companyRepository.save(company); + } + } + +} + + diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StarService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StarService.java new file mode 100644 index 00000000..366dec53 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StarService.java @@ -0,0 +1,47 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.stock.dto.StarResponseDto; +import com.stockholm.main_project.stock.entity.Star; +import com.stockholm.main_project.stock.mapper.StockMapper; +import com.stockholm.main_project.stock.repository.StarRepository; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@Transactional +public class StarService { + private final StarRepository starRepository; + private final CompanyService companyService; + private final StockMapper stockMapper; + + public StarService(StarRepository starRepository, CompanyService companyService, StockMapper stockMapper) { + this.starRepository = starRepository; + this.companyService = companyService; + this.stockMapper = stockMapper; + } + + public void saveStar(Member member, long companyId) { + Star star = new Star(); + + star.setMember(member); + star.setCompany(companyService.findCompanyById(companyId)); + + starRepository.save(star); + } + + public void deleteStar(Member member, long companyId) { + Star star = starRepository.findByMember_MemberIdAndCompanyCompanyId(member.getMemberId(), companyId); + + starRepository.delete(star); + } + + public List getStarResponseDtoList(Member member) { + List stars = starRepository.findAllByMember_MemberId(member.getMemberId()); + List starResponseDtos = stockMapper.starsToStarResponseDtos(stars); + + return starResponseDtos; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockAsBiService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockAsBiService.java new file mode 100644 index 00000000..6c9e9acf --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockAsBiService.java @@ -0,0 +1,70 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.stock.dto.StockasbiDataDto; +import com.stockholm.main_project.stock.entity.Company; +import com.stockholm.main_project.stock.entity.StockAsBi; +import com.stockholm.main_project.stock.mapper.ApiMapper; +import com.stockholm.main_project.stock.repository.StockAsBiRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +@Slf4j +public class StockAsBiService { + + private final StockAsBiRepository stockAsBiRepository; + private final ApiCallService apiCallService; + private final ApiMapper apiMapper; + private final CompanyService companyService; + + public StockAsBiService(StockAsBiRepository stockAsBiRepository, ApiCallService apiCallService, ApiMapper apiMapper, CompanyService companyService) { + this.stockAsBiRepository = stockAsBiRepository; + this.apiCallService = apiCallService; + this.apiMapper = apiMapper; + this.companyService = companyService; + } + + public StockAsBi saveStockAsBi(StockAsBi stockAsBi) { + return stockAsBiRepository.save(stockAsBi); + } + + public void updateStockAsBi() throws InterruptedException { + List companyList = companyService.findCompanies(); + + for(int i = 0; i < companyList.size(); i++) { + // 주식 코드로 회사 불러오기 + Company company = companyService.findCompanyByCode(companyList.get(i).getCode()); + // api 호출하기 + StockasbiDataDto stockasbiDataDto = apiCallService.getStockasbiDataFromApi(company.getCode()); + // mapper로 정리 된 값 받기 + StockAsBi stockAsBi = apiMapper.stockAsBiOutput1ToStockAsBi(stockasbiDataDto.getOutput1()); + + // 회사 등록 + stockAsBi.setCompany(company); + // 호가 컬럼을 새로운 호가 컬럼으로 변경한다 + StockAsBi oldStockAsBi = company.getStockAsBi(); + stockAsBi.setStockAsBiId(oldStockAsBi.getStockAsBiId()); + company.setStockAsBi(stockAsBi); + + // 저장한다 + companyService.saveCompany(company); + + Thread.sleep(500); + } + } + + public StockAsBi getStockAsBi(long companyId) { + Optional stock = stockAsBiRepository.findById(companyId); + stock.orElseThrow(() -> new BusinessLogicException(ExceptionCode.STOCKASBI_NOT_FOUND)); + + return stock.get(); + } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockHoldService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockHoldService.java new file mode 100644 index 00000000..5513ce61 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockHoldService.java @@ -0,0 +1,115 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.member.repository.MemberRepository; +import com.stockholm.main_project.stock.dto.StockHoldResponseDto; +import com.stockholm.main_project.stock.entity.Company; +import com.stockholm.main_project.stock.entity.StockHold; +import com.stockholm.main_project.stock.entity.StockOrder; +import com.stockholm.main_project.stock.mapper.StockMapper; +import com.stockholm.main_project.stock.repository.CompanyRepository; +import com.stockholm.main_project.stock.repository.StockHoldRepository; +import com.stockholm.main_project.stock.repository.StockOrderRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@Transactional +@Slf4j +public class StockHoldService { + private final StockHoldRepository stockHoldRepository; + private final MemberRepository memberRepository; + private final CompanyRepository companyRepository; + private final StockOrderRepository stockOrderRepository; + private final StockMapper stockMapper; + + + public StockHoldService(StockHoldRepository stockHoldRepository, MemberRepository memberRepository, CompanyRepository companyRepository, StockOrderRepository stockOrderRepository, StockMapper stockMapper) { + this.stockHoldRepository = stockHoldRepository; + this.memberRepository = memberRepository; + this.companyRepository = companyRepository; + this.stockOrderRepository = stockOrderRepository; + this.stockMapper = stockMapper; + } + + // 없으면 새로운 스톡 홀드를 생성해서 반환해준다 + public StockHold checkStockHold(long companyId, long memberId) { + StockHold stockHold = stockHoldRepository.findByCompanyCompanyIdAndMemberMemberId(companyId, memberId); + if(stockHold == null) { + StockHold newStockHold = new StockHold(); + newStockHold.setMember(memberRepository.findById(memberId).get()); + newStockHold.setCompany(companyRepository.findById(companyId).get()); + + return newStockHold; + } + else + return stockHold; + } + + // 보유중인 회사 주식 정보 반환 + public StockHold findStockHold(long companyId, long memberId) { + StockHold stockHold = stockHoldRepository.findByCompanyCompanyIdAndMemberMemberId(companyId, memberId); + if(stockHold == null) + throw new BusinessLogicException(ExceptionCode.STOCKHOLD_NOT_FOUND); + else + return stockHold; + } + + public List findStockHolds(long memberId) { + List stockHoldList = stockHoldRepository.findAllByMember_MemberId(memberId); + List stockHoldResponseDtos = stockMapper.stockHoldToStockHoldResponseDto(stockHoldList); +// for(StockHoldResponseDto stockHold : stockHoldResponseDtos) { +// +// List stockOrders = stockOrderRepository +// .findAllByMember_MemberIdAndCompany_CompanyIdAndOrderStatesAndOrderTypes( +// stockHold.getMemberId(), +// stockHold.getCompanyId(), +// StockOrder.OrderStates.ORDER_WAIT, +// StockOrder.OrderTypes.SELL +// ); +// int orderWaitCount = stockOrders.stream().mapToInt(StockOrder::getStockCount).sum(); +// stockHold.setReserveSellStockCount(orderWaitCount); +// } + + return stockHoldResponseDtos; + } + + //수익률 계산하는 로직 + public List setPercentage(List stockHoldResponseDtos) { + for(StockHoldResponseDto stockHoldResponseDto : stockHoldResponseDtos) { + // 이름으로 회사를 불러온다 + Company company = companyRepository.findByCompanyId(stockHoldResponseDto.getCompanyId()); + // 주식 현재가를 불러온다 + String nowPrice = company.getStockInf().getStck_prpr(); + // 주식 수익 = 전체 주식 가치 - 전체 투자 금액 + double totalRevenue = + Double.valueOf(nowPrice) + * (stockHoldResponseDto.getStockCount()+stockHoldResponseDto.getReserveSellStockCount()) + - stockHoldResponseDto.getTotalPrice(); + // 주식 수익률(%) = (주식 수익 / 전체 투자 금액) × 100 + double percentage = (totalRevenue / (double)stockHoldResponseDto.getTotalPrice()) * 100; + + stockHoldResponseDto.setPercentage(percentage); + stockHoldResponseDto.setStockReturn((long) totalRevenue); + } + return stockHoldResponseDtos; + } + + //보유 주식 전부 삭제하는 로직 + public void deleteStockHolds(long memberId) { + List stockHolds = getMemberStockHolds(memberId); + + stockHoldRepository.deleteAll(stockHolds); + } + + public List getMemberStockHolds(long memberId) { + List stockHolds = stockHoldRepository.findAllByMember_MemberId(memberId); + + return stockHolds; + } + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockMinService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockMinService.java new file mode 100644 index 00000000..f6f7f25e --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockMinService.java @@ -0,0 +1,94 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.stock.dto.StockMinDto; +import com.stockholm.main_project.stock.dto.StockMinResponseDto; +import com.stockholm.main_project.stock.entity.Company; +import com.stockholm.main_project.stock.entity.StockInf; +import com.stockholm.main_project.stock.entity.StockMin; +import com.stockholm.main_project.stock.mapper.StockMapper; +import com.stockholm.main_project.stock.mapper.ApiMapper; +import com.stockholm.main_project.stock.repository.StockMinRepository; +import com.stockholm.main_project.utils.Time; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Transactional +@Slf4j +public class StockMinService { + + private final CompanyService companyService; + private final ApiCallService apiCallService; + private final ApiMapper apiMapper; + private final StockMinRepository stockMinRepository; + private final StockMapper stockMapper; + + public StockMinService(CompanyService companyService, ApiCallService apiCallService, ApiMapper apiMapper, StockMinRepository stockMinRepository, StockMapper stockMapper) { + this.companyService = companyService; + this.apiCallService = apiCallService; + this.apiMapper = apiMapper; + this.stockMinRepository = stockMinRepository; + this.stockMapper = stockMapper; + } + + public void updateStockMin() throws InterruptedException { + List companyList = companyService.findCompanies(); + LocalDateTime now = LocalDateTime.now(); + String strHour = Time.strHour(now); + + + for(int i = 0; i < companyList.size(); i++) { + // 주식 코드로 회사 불러오기 + Company company = companyService.findCompanyByCode(companyList.get(i).getCode()); + // 분봉 api 호출하기 + StockMinDto stockMinDto = apiCallService.getStockMinDataFromApi(company.getCode(), strHour); + // mapper로 정리 된 값 받기 + List stockMinList = stockMinDto.getOutput2().stream() + .map(stockMinOutput2 -> { + StockMin stockMin = apiMapper.stockMinOutput2ToStockMin(stockMinOutput2); + stockMin.setCompany(company); + stockMin.setTradeTime(now); + return stockMin; + }).collect(Collectors.toList()); + // 빠른 시간 순으로 정렬 + Collections.sort(stockMinList, Comparator.comparing(StockMin::getStockTradeTime)); + // 회사 정보 저장 + StockInf stockInf = apiMapper.stockMinOutput1ToStockInf(stockMinDto.getOutput1()); + stockInf.setCompany(company); + StockInf oldStockInf = company.getStockInf(); + stockInf.setStockInfId(oldStockInf.getStockInfId()); + company.setStockInf(stockInf); + + // 저장한다 + stockMinRepository.saveAll(stockMinList); + companyService.saveCompany(company); + + + Thread.sleep(500); + } + } + + public List getChart(long companyId) { + List stockMinList = stockMinRepository.findAllByCompanyCompanyId(companyId); + + return stockMinList; + } + + public List getRecent420StockMin(long companyId) { + List stockMinList = stockMinRepository.findTop420ByCompanyIdOrderByStockMinIdDesc(companyId); + + List stockMinResponseDtos = stockMinList.stream() + .map(stockMin -> stockMapper.stockMinToStockMinResponseDto(stockMin)).collect(Collectors.toList()); + Collections.reverse(stockMinResponseDtos); + return stockMinResponseDtos; + } + + +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockOrderService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockOrderService.java new file mode 100644 index 00000000..9b8e9c72 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/StockOrderService.java @@ -0,0 +1,455 @@ +package com.stockholm.main_project.stock.service; + +import java.lang.management.ManagementFactory; + +import com.stockholm.main_project.cash.entity.Cash; +import com.stockholm.main_project.cash.service.CashService; +import com.stockholm.main_project.exception.BusinessLogicException; +import com.stockholm.main_project.exception.ExceptionCode; +import com.stockholm.main_project.member.entity.Member; +import com.stockholm.main_project.member.repository.MemberRepository; +import com.stockholm.main_project.stock.controller.LongPollingController; +import com.stockholm.main_project.stock.dto.StockOrderResponseDto; +import com.stockholm.main_project.stock.entity.Company; +import com.stockholm.main_project.stock.entity.StockAsBi; +import com.stockholm.main_project.stock.entity.StockHold; +import com.stockholm.main_project.stock.entity.StockOrder; +import com.stockholm.main_project.stock.mapper.StockMapper; +import com.stockholm.main_project.stock.repository.StockHoldRepository; +import com.stockholm.main_project.stock.repository.StockOrderRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional +@Slf4j +public class StockOrderService { + private final StockAsBiService stockAsBiService; + private final CompanyService companyService; + private final StockOrderRepository stockOrderRepository; + private final MemberRepository memberRepository; + private final StockHoldService stockHoldService; + private final StockHoldRepository stockHoldRepository; + private final CashService cashService; + private final StockMapper stockMapper; + private final LongPollingController longPollingController; + private final SimpMessagingTemplate messagingTemplate; + + public StockOrderService(StockAsBiService stockAsBiService, CompanyService companyService, StockOrderRepository stockOrderRepository, MemberRepository memberRepository, StockHoldService stockHoldService, StockHoldRepository stockHoldRepository, CashService cashService, StockMapper stockMapper, LongPollingController longPollingController, SimpMessagingTemplate messagingTemplate) { + this.stockAsBiService = stockAsBiService; + this.companyService = companyService; + this.stockOrderRepository = stockOrderRepository; + this.memberRepository = memberRepository; + this.stockHoldService = stockHoldService; + this.stockHoldRepository = stockHoldRepository; + this.cashService = cashService; + this.stockMapper = stockMapper; + this.longPollingController = longPollingController; + this.messagingTemplate = messagingTemplate; + } + + // 멤버, 회사 id, 가격 + public StockOrder buyStocks(Member member, long companyId, long price, int stockCount) { + //회원 캐쉬 잔량 비교 + cashService.checkCash(price * stockCount, member); // -> 부족할 시 예외 처리 + //호가 불러오기 + StockAsBi stockAsBi = stockAsBiService.getStockAsBi(companyId); + // 예약 구매인지 바로 구매인지 판별 + return buyDiscrimination(member, price, stockAsBi, stockCount, companyId); + } + + private StockOrder buyDiscrimination(Member member, long price, StockAsBi stockAsBi, int stockCount, long companyId) { + // 매도 호가와 가격이 같고, 잔량이 남아 있을 경우 + if(Long.parseLong(stockAsBi.getAskp1()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn1()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp2()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn2()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp3()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn3()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp4()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn4()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp5()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn5()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp6()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn6()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp7()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn7()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp8()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn8()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp9()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn9()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp10()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn10()) > stockCount) + return buyStock(member, price, stockCount, companyId); // 구매 로직 + else + return reserveStock(member, price, stockCount, companyId, StockOrder.OrderTypes.BUY); //예약 구매 로직 + } + + public StockOrder buyStock(Member member, long price, int stockCount, long companyId) { + // 보유 주식 설정 + StockHold stockHold = stockHoldService.checkStockHold(companyId, member.getMemberId()); + stockHold.setStockCount(stockHold.getStockCount() + stockCount); + stockHold.setPrice(stockHold.getPrice() + (stockCount * price)); + + // 스톡 오더 작성 + StockOrder stockOrder = new StockOrder(); + stockOrder.setOrderStates(StockOrder.OrderStates.ORDER_COMPLETE); + stockOrder.setOrderTypes(StockOrder.OrderTypes.BUY); + stockOrder.setStockCount(stockCount); + stockOrder.setPrice(price); + stockOrder.setCompany(companyService.findCompanyById(companyId)); + + // 현금량 감소 + Cash cash = member.getCash(); + cash.setMoney(cash.getMoney()-(price * stockCount)); + member.setCash(cash); + stockOrder.setMember(member); + + stockOrderRepository.save(stockOrder); + memberRepository.save(member); + stockHoldRepository.save(stockHold); + + + return stockOrder; + } + + // 예약 매도 일 때는 보유 주식 줄어들게(완료) + public StockOrder reserveStock(Member member, long price, int stockCount, long companyId, StockOrder.OrderTypes types) { + if(StockOrder.OrderTypes.SELL.equals(types)) { + // 보유 주식 설정 + StockHold stockHold = stockHoldService.findStockHold(companyId, member.getMemberId()); + stockHold.setStockCount(stockHold.getStockCount() - stockCount); + stockHold.setReserveStockCount(stockCount); + + } + StockOrder stockOrder = new StockOrder(); + stockOrder.setOrderStates(StockOrder.OrderStates.ORDER_WAIT); + stockOrder.setOrderTypes(types); + stockOrder.setStockCount(stockCount); + stockOrder.setPrice(price); + stockOrder.setCompany(companyService.findCompanyById(companyId)); + stockOrder.setMember(member); + + stockOrderRepository.save(stockOrder); + + return stockOrder; + } + + public void checkOrder() { + // 회사 리스트를 받아온다 + List companyList = companyService.findCompanies(); + List updateBuyStockOrders = new ArrayList<>(); + List updateSellStockOrders = new ArrayList<>(); + // for문(회사별로) + for(Company company : companyList) { + // 회사 호가 리스트를 받아온다 + StockAsBi stockAsBi = stockAsBiService.getStockAsBi(company.getCompanyId()); + // 회사Id에 있는 stockOrder 중 체결 대기 상태인 stockOrder를 큐로 받아온다 + Queue stockOrderQueue = getStockOrderQueue(company.getCompanyId(), StockOrder.OrderStates.ORDER_WAIT); + //큐가 비어있지 않으면 + if(!stockOrderQueue.isEmpty()) { + // for문(큐가 다 빌 때 까지 실행한다) + while(!stockOrderQueue.isEmpty()) { + StockOrder stockOrder = stockOrderQueue.poll(); + // 예약 매수 실행 + if(stockOrder.getOrderTypes().equals(StockOrder.OrderTypes.BUY)) { + // 호가 리스트 안에 체결 대기중인 stockOrder의 조건이 맞는 것이 있으면 buyStock으로 간다 + StockOrder buyStock = reserveBuyDiscrimination(stockAsBi, stockOrder); + // 클라이언트로 StockOrder를 보낸다(값이 있으면) + if(buyStock != null) + updateBuyStockOrders.add(buyStock); + } + // 예약 매도 실행 + else { + StockOrder sellStock = reserveSellDiscrimination(stockAsBi, stockOrder); + // 클라이언트로 StockOrder를 보낸다(값이 있으면) + if(sellStock != null) + updateSellStockOrders.add(sellStock); + } + } + } + } + long activeThreadCount = ManagementFactory.getThreadMXBean().getThreadCount(); + System.out.println("현재 활성 스레드 수: " + activeThreadCount); + sendStockOrder(updateBuyStockOrders, updateSellStockOrders); + //longPollingController.notifyDataUpdated(updateBuyStockOrders, updateSellStockOrders); + } + + + + private StockOrder reserveBuyDiscrimination(StockAsBi stockAsBi, StockOrder stockOrder) { + long price = stockOrder.getPrice(); + int stockCount = stockOrder.getStockCount(); + // 매도 호가와 가격이 같고, 잔량이 남아 있을 경우 + if(Long.parseLong(stockAsBi.getAskp1()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn1()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp2()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn2()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp3()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn3()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp4()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn4()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp5()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn5()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp6()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn6()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp7()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn7()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp8()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn8()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp9()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn9()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else if(Long.parseLong(stockAsBi.getAskp10()) == price && Integer.parseInt(stockAsBi.getAskp_rsqn10()) > stockCount) + return reserveBuyStock(stockOrder); // 구매 로직 + else { + return null; // 아무것도 안함 + } + + } + + private StockOrder reserveSellDiscrimination(StockAsBi stockAsBi, StockOrder stockOrder) { + long price = stockOrder.getPrice(); + int stockCount = stockOrder.getStockCount(); + // 매도 호가와 가격이 같고, 잔량이 남아 있을 경우 + if(Long.parseLong(stockAsBi.getBidp1()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn1()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp2()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn2()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp3()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn3()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp4()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn4()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp5()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn5()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp6()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn6()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp7()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn7()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp8()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn8()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp9()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn9()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp10()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn10()) > stockCount) + return reserveSellStock(stockOrder); // 판매 로직 + else + return null; // 아무것도 안함 + + } + + // 예약된 매수 -> 구매 상태로 바구기 + public StockOrder reserveBuyStock(StockOrder stockOrder) { + Optional optionalStockOrder = stockOrderRepository.findById(stockOrder.getStockOrderId()); + StockOrder updateStockOrder = optionalStockOrder.get(); + updateStockOrder.setOrderStates(StockOrder.OrderStates.ORDER_COMPLETE); + updateStockOrder.setOrderTypes(StockOrder.OrderTypes.BUY); + // 보유 주식 설정 + StockHold stockHold = stockHoldService.checkStockHold(stockOrder.getCompany().getCompanyId(), stockOrder.getMember().getMemberId()); + stockHold.setStockCount(stockHold.getStockCount() + stockOrder.getStockCount()); + stockHold.setPrice(stockHold.getPrice() + (stockOrder.getStockCount() * stockOrder.getPrice())); + // 현금량 감소 + Member member = updateStockOrder.getMember(); + Cash cash = member.getCash(); + cash.setMoney(cash.getMoney()-(stockOrder.getPrice() * stockOrder.getStockCount())); + member.setCash(cash); + stockOrder.setMember(member); + + stockOrderRepository.save(stockOrder); + memberRepository.save(member); + stockHoldRepository.save(stockHold); + + return stockOrder; + } + + // 예약 매도 -> 판매로 바뀔 때 금액 늘어나게, 보유 주식은 예약 할 때 줄어들도록 + // Price 줄어드는 금액은 주식 투자금액 - (주식 투자 금액 / 보유 주식 개수) * 팔 주식 개수 (완료) + public StockOrder reserveSellStock(StockOrder stockOrder) { + Optional optionalStockOrder = stockOrderRepository.findById(stockOrder.getStockOrderId()); + StockOrder updateStockOrder = optionalStockOrder.get(); + updateStockOrder.setOrderStates(StockOrder.OrderStates.ORDER_COMPLETE); + updateStockOrder.setOrderTypes(StockOrder.OrderTypes.SELL); + // 보유 주식 설정 + StockHold stockHold = stockHoldService.findStockHold(stockOrder.getCompany().getCompanyId(), stockOrder.getMember().getMemberId()); + stockHold.setPrice(stockHold.getPrice() - (stockHold.getPrice() / (stockHold.getStockCount()+stockHold.getReserveStockCount())) * stockOrder.getStockCount()); + stockHold.setReserveStockCount(stockHold.getReserveStockCount() - stockOrder.getStockCount()); + // 현금량 증가 + Member member = updateStockOrder.getMember(); + Cash cash = member.getCash(); + cash.setMoney(cash.getMoney() + (stockOrder.getPrice() * stockOrder.getStockCount())); + member.setCash(cash); + stockOrder.setMember(member); + + stockOrderRepository.save(stockOrder); + memberRepository.save(member); + if(stockHold.getStockCount() + stockHold.getReserveStockCount() == 0) + stockHoldRepository.delete(stockHold); + else + stockHoldRepository.save(stockHold); + + return stockOrder; + } + + // 멤버, 회사 id, 가격 + public StockOrder sellStocks(Member member, long companyId, long price, int stockCount) { + // 내가 주식을 가지고 있는지 없는지 판별 + StockHold stockHold = stockHoldService.findStockHold(companyId, member.getMemberId()); + if(stockHold.getStockCount() < stockCount) + throw new BusinessLogicException(ExceptionCode.INSUFFICIENT_STOCK); + else { + //호가 불러오기 + StockAsBi stockAsBi = stockAsBiService.getStockAsBi(companyId); + // 예약 판매인지 바로 판매인지 판별 + return sellDiscrimination(member, price, stockAsBi, stockCount, companyId); + } + + } + + // 매도 판별해서 실행 + public StockOrder sellDiscrimination(Member member, long price, StockAsBi stockAsBi, int stockCount, long companyId) { + // 매도 호가와 가격이 같고, 잔량이 남아 있을 경우 + if(Long.parseLong(stockAsBi.getBidp1()) == price && Integer.parseInt(stockAsBi.getBidp1()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp2()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn2()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp3()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn3()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp4()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn4()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp5()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn5()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp6()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn6()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp7()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn7()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp8()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn8()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp9()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn9()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else if(Long.parseLong(stockAsBi.getBidp10()) == price && Integer.parseInt(stockAsBi.getBidp_rsqn10()) > stockCount) + return sellStock(member, price, stockCount, companyId); // 판매 로직 + else + return reserveStock(member, price, stockCount, companyId, StockOrder.OrderTypes.SELL); //예약 판매 로직 + } + + public StockOrder sellStock(Member member, long price, int stockCount, long companyId) { + // 보유 주식 설정 + StockHold stockHold = stockHoldService.findStockHold(companyId, member.getMemberId()); + stockHold.setPrice(stockHold.getPrice() - (stockHold.getPrice() / (stockHold.getStockCount()+stockHold.getReserveStockCount())) * stockCount); + stockHold.setStockCount(stockHold.getStockCount() - stockCount); + + // 스톡 오더 작성 + StockOrder stockOrder = new StockOrder(); + stockOrder.setOrderStates(StockOrder.OrderStates.ORDER_COMPLETE); + stockOrder.setOrderTypes(StockOrder.OrderTypes.SELL); + stockOrder.setStockCount(stockCount); + stockOrder.setPrice(price); + stockOrder.setCompany(companyService.findCompanyById(companyId)); + + // 현금량 증가 + Cash cash = member.getCash(); + cash.setMoney(cash.getMoney()+(price * stockCount)); + member.setCash(cash); + stockOrder.setMember(member); + + + + stockOrderRepository.save(stockOrder); + memberRepository.save(member); + // 여기서 stockhold 삭제됨 + // 예약 매도 걸 때 reserveStockCount 늘어가네 + // 예약 매도 취소 할 때 reserveStockCount 줄어들게 + if(stockHold.getStockCount() + stockHold.getReserveStockCount() == 0) + stockHoldRepository.delete(stockHold); + else + stockHoldRepository.save(stockHold); + + return stockOrder; + } + + + // 거래 대기중인 매수 StockOrder 불러오기 + public Queue getStockOrderQueue(long companyId, StockOrder.OrderStates orderStates) { + List stockOrderList = stockOrderRepository.findAllByCompanyCompanyIdAndOrderStates(companyId, orderStates); + Queue stockOrderQueue = new LinkedList<>(stockOrderList); + + return stockOrderQueue; + } + + // 멤버의 모든 주식 거래내역 삭제하기 + public void deleteStockOrders(Member member) { + List stockOrders = stockOrderRepository.findAllByMember_MemberId(member.getMemberId()); + + stockOrderRepository.deleteAll(stockOrders); + } + + // 멤버의 모든 StockOrders 불러오기 + public List getMemberStockOrders(long memberId) { + List stockOrders = stockOrderRepository.findAllByMember_MemberIdOrderByModifiedAtDesc(memberId); + List stockOrderRepositories = stockOrders.stream() + .map(stockOrder -> stockMapper.stockOrderToStockOrderResponseDto(stockOrder)).collect(Collectors.toList()); + + return stockOrderRepositories; + } + + // 미체결 stockOrder 취소하는 메소드 + // 취소한 주식 수 만큼 보유주식으로 돌아오게 해야함 + public void deleteStockOrder(Member member, long stockOrderId, int stockCount) { + Optional optionalStockOrder = stockOrderRepository.findById(stockOrderId); + StockOrder stockOrder = optionalStockOrder.orElseThrow(() -> new BusinessLogicException(ExceptionCode.STOCKORDER_NOT_FOUND)); + + if(stockOrder.getMember().getMemberId() != member.getMemberId()) { + throw new BusinessLogicException(ExceptionCode.STOCKORDER_PERMISSION_DENIED); + } + else if(!stockOrder.getOrderStates().equals(StockOrder.OrderStates.ORDER_WAIT)) + throw new BusinessLogicException(ExceptionCode.STOCKORDER_ALREADY_FINISH); + // 수량 선택해서 취소 할 수 있게(취소한 만큼 보유 주식 돌아오게) 0이 되면 미체결 스톡 오더 삭제 + else { + if(stockOrder.getStockCount() <= stockCount) + stockOrderRepository.delete(stockOrder); + else { + stockOrder.setStockCount(stockOrder.getStockCount() - stockCount); + } + if(StockOrder.OrderTypes.SELL.equals(stockOrder.getOrderTypes())) { + StockHold stockHold = stockHoldService.findStockHold(stockOrder.getCompany().getCompanyId(), stockOrder.getMember().getMemberId()); + stockHold.setStockCount(stockHold.getStockCount() + stockCount); + stockHold.setReserveStockCount(stockHold.getReserveStockCount() - stockCount); + } + + } + } + // 클라언트에 알림 보냄 + public void sendStockOrder(List updateBuyStockOrders, List updateSellStockOrders) { + Map> buyStockOrdersByMemberId = updateBuyStockOrders.stream() + .collect(Collectors.groupingBy(stockOrder -> stockOrder.getMember().getMemberId())); + Map> sellStockOrdersByMemberId = updateSellStockOrders.stream() + .collect(Collectors.groupingBy(stockOrder -> stockOrder.getMember().getMemberId())); + + buyStockOrdersByMemberId.forEach((memberId, orders) -> { + System.out.println("Member ID: " + memberId); + orders.forEach(order -> { + String destination = "/sub/" + memberId; + StockOrder stockOrder = order; + StockOrderResponseDto stockOrderResponseDto = stockMapper.stockOrderToStockOrderResponseDto(stockOrder); + + messagingTemplate.convertAndSend(destination, stockOrderResponseDto); + + }); + }); + + sellStockOrdersByMemberId.forEach((memberId, orders) -> { + System.out.println("Member ID: " + memberId); + orders.forEach(order -> { + String destination = "/sub/" + memberId; + System.out.println(destination); + StockOrder stockOrder = order; + StockOrderResponseDto stockOrderResponseDto = stockMapper.stockOrderToStockOrderResponseDto(stockOrder); + + messagingTemplate.convertAndSend(destination, stockOrderResponseDto); + + }); + }); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/TokenService.java b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/TokenService.java new file mode 100644 index 00000000..0785e543 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/stock/service/TokenService.java @@ -0,0 +1,101 @@ +package com.stockholm.main_project.stock.service; + +import com.stockholm.main_project.stock.repository.TokenRepository; +import com.stockholm.main_project.stock.entity.Token; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Service +@Transactional +public class TokenService { + + public TokenService(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + private final TokenRepository tokenRepository; + + @Getter + @Value("${token.app-key}") + private String APP_KEY; + + @Getter + @Value("${token.app-secret}") + private String APP_SECRET; + + @Getter + @Value("${stock-url.token}") + private String TOKEN_URL; + + private RestTemplate restTemplate = new RestTemplate(); + + public String getAccessToken() { + + // 만료 되지 않았을 경우 + if(tokenVerification()) { + Optional token = tokenRepository.findById(1L); + + return token.get().getToken(); + + } + // 만료 되었을 경우 + else { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map body = new HashMap<>(); + body.put("grant_type", "client_credentials"); + body.put("appkey", APP_KEY); + body.put("appsecret", APP_SECRET); + + HttpEntity> request = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.postForEntity(TOKEN_URL, request, Map.class); + + Optional token = tokenRepository.findById(1L); + + if(token.isEmpty()) { + Token newToken = new Token(); + + newToken.setTokenId(1L); + newToken.setToken(response.getBody().get("access_token").toString()); + newToken.setExpired(LocalDateTime.now().plusDays(1)); + + tokenRepository.save(newToken); + + } + else { + token.get().setToken(response.getBody().get("access_token").toString()); + token.get().setExpired(LocalDateTime.now().plusDays(1)); + + tokenRepository.save(token.get()); + + } + + return response.getBody().get("access_token").toString(); + } + } + + public boolean tokenVerification() { + LocalDateTime currentDateTime = LocalDateTime.now(); + Optional token = tokenRepository.findById(1L); + + // 토큰이 비어있거나, 현재 시간이 토큰 유효간보다 뒤에 있을 때(만료 됨) + if( token.isEmpty() || currentDateTime.isAfter(token.get().getExpired())) + return false; + else + return true; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloController.java b/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloController.java new file mode 100644 index 00000000..e478d320 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloController.java @@ -0,0 +1,55 @@ +package com.stockholm.main_project.swaggersample; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/stockholm") +public class HelloController { + + @Operation(summary = "hello post 요청", description = "post됩니다.", tags = { "Member" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = HelloResponse.class))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + @PostMapping + public ResponseEntity helloPost( + @RequestBody HelloPostDto helloPostDto) { + HelloResponse helloResponse = new HelloResponse(); + + helloResponse.setId(1); + helloResponse.setText(helloResponse.getText()); + + return new ResponseEntity<>(helloResponse, HttpStatus.OK); + } + + @Operation(summary = "hello get 요청", description = "get됩니다.", tags = { "Member" }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = HelloResponse.class))), + @ApiResponse(responseCode = "400", description = "BAD REQUEST"), + @ApiResponse(responseCode = "404", description = "NOT FOUND"), + @ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR") + }) + @GetMapping("{id}") + public ResponseEntity helloGet( + @Parameter(description = "id값") + @PathVariable("id") String id) { + HelloResponse helloResponse = new HelloResponse(); + + helloResponse.setId(1); + helloResponse.setText("hello"); + + return new ResponseEntity<>(helloResponse, HttpStatus.OK); + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloPostDto.java b/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloPostDto.java new file mode 100644 index 00000000..9e64411a --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloPostDto.java @@ -0,0 +1,12 @@ +package com.stockholm.main_project.swaggersample; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HelloPostDto { + @Schema(description = "내용", defaultValue = "hello") + private String text; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloResponse.java b/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloResponse.java new file mode 100644 index 00000000..f4c1b438 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/swaggersample/HelloResponse.java @@ -0,0 +1,16 @@ +package com.stockholm.main_project.swaggersample; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class HelloResponse { + @Schema(description = "아이디", defaultValue = "1") + private long id; + @Schema(description = "내용", defaultValue = "hello") + private String text; +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/utils/Time.java b/server/008main_project/src/main/java/com/stockholm/main_project/utils/Time.java new file mode 100644 index 00000000..81e7d828 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/utils/Time.java @@ -0,0 +1,25 @@ +package com.stockholm.main_project.utils; + +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Component +public class Time { + public static String strHour(LocalDateTime localDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HHmm"); + String formattedDateTime = localDateTime.format(formatter); + + formattedDateTime = formattedDateTime.concat("00"); + + return formattedDateTime; + } + + public static String strMonth(LocalDateTime localDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + String formattedDateTime = localDateTime.format(formatter); + + return formattedDateTime; + } +} diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/websocket/StompConfig.java b/server/008main_project/src/main/java/com/stockholm/main_project/websocket/StompConfig.java new file mode 100644 index 00000000..f1c9d717 --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/websocket/StompConfig.java @@ -0,0 +1,25 @@ +package com.stockholm.main_project.websocket; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class StompConfig implements WebSocketMessageBrokerConfigurer { + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws").withSockJS(); + registry.addEndpoint("/ws").setAllowedOrigins("*"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 메세지 구독 요청 url -> 메세지 받을 때 + registry.enableSimpleBroker("/sub"); + // 메세지 발행 요청 url -> 메세지 보낼 때 + registry.setApplicationDestinationPrefixes("/pub"); + } +} \ No newline at end of file diff --git a/server/008main_project/src/main/java/com/stockholm/main_project/websocket/WebSocketController.java b/server/008main_project/src/main/java/com/stockholm/main_project/websocket/WebSocketController.java new file mode 100644 index 00000000..481d524e --- /dev/null +++ b/server/008main_project/src/main/java/com/stockholm/main_project/websocket/WebSocketController.java @@ -0,0 +1,24 @@ +package com.stockholm.main_project.websocket; + +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; + +@Controller +public class WebSocketController { + + private final SimpMessagingTemplate messagingTemplate; + + public WebSocketController(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + } + + // 서버에서 독단적으로 실행되는 로직 (예: 주문 처리) + public void check() { + // 주문 처리 로직 작성 + + // 클라이언트로 메시지를 보내기 + messagingTemplate.convertAndSend("/sub", "주문이 처리되었습니다."); + } +} diff --git a/server/008main_project/src/main/resources/application.yml b/server/008main_project/src/main/resources/application.yml new file mode 100644 index 00000000..0e1f8de1 --- /dev/null +++ b/server/008main_project/src/main/resources/application.yml @@ -0,0 +1,114 @@ +spring: + datasource: + url: jdbc:mysql://stockholm.c9tdmcjx6fcv.ap-northeast-2.rds.amazonaws.com/stockholm?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL5InnoDBDialect + format_sql: true +# h2: +# console: +# enabled: true +# path: /h2 +# datasource: +# url: jdbc:h2:mem:test +# jpa: +# hibernate: +# ddl-auto: create +# show-sql: true + mail: + host: smtp.gmail.com + port: 587 + username: projectstockholm0@gmail.com + password: kqnonrrvrrldwzlb + properties: + mail: + smtp: + auth: true + starttls: + enable: true + security: + oauth2: + client: + registration: + google: + clientId: 47991654230-k0vku25t92sageg12thm05nfm9nro1rh.apps.googleusercontent.com + clientSecret: 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 + #redirect-uri: "http://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com:8080/login/oauth2/code/google" + scope: + - email + - profile + kakao: + client-id: 9d753ec8e382f0fb6afc1ef276b0996c + client-secret: Gjsd62HCluAllQZaWCYoPEzEe6QAs5wF + client-name: Kakao + client-authentication-method: POST + redirect-uri: "http://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com:8080/login/oauth2/code/kakao" +# redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" + authorization-grant-type: authorization_code + scope: + - profile_nickname + - account_email + provider: + kakao: + authorizationUri: https://kauth.kakao.com/oauth/authorize + tokenUri: https://kauth.kakao.com/oauth/token + userInfoUri: https://kapi.kakao.com/v2/user/me + userNameAttribute: id + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + + jmx: + enabled: true + +springdoc: + swagger-ui: + path: /swagger-ui.html + groups-order: DESC + operationsSorter: method + disable-swagger-default-url: true + display-request-duration: true + api-docs: + path: /api-docs + show-actuator: true + default-consumes-media-type: application/json + default-produces-media-type: application/json + paths-to-match: + - /** + +cloud: + aws: + credentials: + access-key: ${AWS_S3_ACCESS_KEY} + secret-key: ${AWS_S3_SECRET_KEY} + s3: + bucket: "stockholm-server" + region: + static: "ap-northeast-2" + stack: + auto: false + +token: + app-key: "PSjMh9iyz0EFvbpWkvuYHfLiFuHyNAtLoG9h" + app-secret: "vtzv7bG78qgtThPEujr1MWDJHKTawSoEDRfAJzB/lYvwj67HdzUsyUavVGD4kORIeGS5q6BJBwoICXy97h8d3RaSAvhK03Yu/seFm0t+22ZQBv4GKhxvU5jGwdMrsucyKuQ0EtXfkJxJoLFsqIO1UA1n3r4HX0D5RxIe8I8efwEYVbidAn4=" + +stock-url: + token: "https://openapivts.koreainvestment.com:29443/oauth2/tokenP" + stockasbi: "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn" + stockhour: "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice" + kospi: "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-daily-indexchartprice" + +jwt: + key: ${JWT_SECRET_KEY} + access-token-expiration-minutes: 40 + refresh-token-expiration-minutes: 420 + diff --git a/server/008main_project/src/main/resources/stockholmMail.properties b/server/008main_project/src/main/resources/stockholmMail.properties new file mode 100644 index 00000000..65dfaf21 --- /dev/null +++ b/server/008main_project/src/main/resources/stockholmMail.properties @@ -0,0 +1,10 @@ +mail.smtp.auth=true +mail.smtp.starttls.required=true +mail.smtp.starttls.enable=true +mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory +mail.smtp.socketFactory.fallback=false +mail.smtp.port=465 +mail.smtp.socketFactory.port=465 + +AdminMail.id = projectstockholm0@gmail.com +AdminMail.password = kqnonrrvrrldwzlb \ No newline at end of file diff --git a/server/008main_project/src/main/resources/templates/index.html b/server/008main_project/src/main/resources/templates/index.html new file mode 100644 index 00000000..fc4540c6 --- /dev/null +++ b/server/008main_project/src/main/resources/templates/index.html @@ -0,0 +1,12 @@ + + + + + OAuth2 + JWT Frontend + + +

Welcome to OAuth 2.0 + JWT Spring Security

+Google Login +Kakao Login + + \ No newline at end of file diff --git a/server/008main_project/src/main/resources/templates/my-page.html b/server/008main_project/src/main/resources/templates/my-page.html new file mode 100644 index 00000000..de155ea5 --- /dev/null +++ b/server/008main_project/src/main/resources/templates/my-page.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/server/008main_project/src/main/resources/templates/receive-tocken.html b/server/008main_project/src/main/resources/templates/receive-tocken.html new file mode 100644 index 00000000..67d8c958 --- /dev/null +++ b/server/008main_project/src/main/resources/templates/receive-tocken.html @@ -0,0 +1,12 @@ + + + + + OAuth2 + JWT My page + + + + + \ No newline at end of file diff --git a/server/008main_project/src/test/java/com/stockholm/main_project/ApplicationTests.java b/server/008main_project/src/test/java/com/stockholm/main_project/ApplicationTests.java new file mode 100644 index 00000000..a16670e4 --- /dev/null +++ b/server/008main_project/src/test/java/com/stockholm/main_project/ApplicationTests.java @@ -0,0 +1,13 @@ +package com.stockholm.main_project; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/server/READMESERVER.md b/server/READMESERVER.md index d30d87e2..e69de29b 100644 --- a/server/READMESERVER.md +++ b/server/READMESERVER.md @@ -1 +0,0 @@ -서버 전용공간입니다. \ No newline at end of file