diff --git a/client/src/components/test.js b/Backend/.gitattributes similarity index 100% rename from client/src/components/test.js rename to Backend/.gitattributes diff --git a/Backend/.gitignore b/Backend/.gitignore new file mode 100644 index 0000000..10364db --- /dev/null +++ b/Backend/.gitignore @@ -0,0 +1,38 @@ +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/Backend/build.gradle b/Backend/build.gradle new file mode 100644 index 0000000..667cb7e --- /dev/null +++ b/Backend/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'org.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-web-services' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'mysql:mysql-connector-java:8.0.33' // MySQL Connector/J + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA + runtimeOnly 'mysql:mysql-connector-java' // MySQL 연결 드라이버 + implementation 'mysql:mysql-connector-java:8.0.33' + + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-ui:1.5.9' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.webjars:webjars-locator-core:0.48' + + implementation 'io.github.classgraph:classgraph:4.8.47' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/Backend/gradle/wrapper/gradle-wrapper.jar b/Backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/Backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Backend/gradle/wrapper/gradle-wrapper.properties b/Backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..45a79d2 --- /dev/null +++ b/Backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists + diff --git a/Backend/gradlew b/Backend/gradlew new file mode 100644 index 0000000..f5feea6 --- /dev/null +++ b/Backend/gradlew @@ -0,0 +1,252 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Backend/gradlew.bat b/Backend/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/Backend/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Backend/settings.gradle b/Backend/settings.gradle new file mode 100644 index 0000000..952a612 --- /dev/null +++ b/Backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'web3_spring2' diff --git a/Backend/src/main/java/com/web3/Backend/Web3Spring2Application.java b/Backend/src/main/java/com/web3/Backend/Web3Spring2Application.java new file mode 100644 index 0000000..e5999fc --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/Web3Spring2Application.java @@ -0,0 +1,13 @@ +package com.web3.Backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Web3Spring2Application { + + public static void main(String[] args) { + SpringApplication.run(Web3Spring2Application.class, args); + } + +} diff --git a/Backend/src/main/java/com/web3/Backend/config/SecurityConfig.java b/Backend/src/main/java/com/web3/Backend/config/SecurityConfig.java new file mode 100644 index 0000000..4cc7e80 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/config/SecurityConfig.java @@ -0,0 +1,114 @@ +package com.web3.Backend.config; + +import com.web3.Backend.jwt.CustomLogoutFilter; +import com.web3.Backend.jwt.JWTFilter; +import com.web3.Backend.jwt.JWTUtil; +import com.web3.Backend.jwt.LoginFilter; +import com.web3.Backend.repository.RefreshRepository; +import com.web3.Backend.service.CustomUserDetailsService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final AuthenticationConfiguration authenticationConfiguration; + private final JWTUtil jwtUtil; + private final RefreshRepository refreshRepository; + private final CustomUserDetailsService customUserDetailsService; + + public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, + JWTUtil jwtUtil, + RefreshRepository refreshRepository, + CustomUserDetailsService customUserDetailsService) { + this.authenticationConfiguration = authenticationConfiguration; + this.jwtUtil = jwtUtil; + this.refreshRepository = refreshRepository; + this.customUserDetailsService = customUserDetailsService; + } + + // AuthenticationManager Bean 정의 + @Bean + public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = + http.getSharedObject(AuthenticationManagerBuilder.class); + + //UserDetailsService와 PasswordEncoder 연결 + authenticationManagerBuilder.userDetailsService(customUserDetailsService) + .passwordEncoder(bCryptPasswordEncoder()); + + return authenticationManagerBuilder.build(); + } + + // BCryptPasswordEncoder Bean 정의 + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + // SecurityFilterChain Bean 정의 + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 명시적으로 CORS 설정 + // CSRF 비활성화 + .csrf(csrf -> csrf.disable()) + // 폼 로그인 비활성화 + .formLogin(form -> form.disable()) + // HTTP Basic 인증 비활성화 + .httpBasic(httpBasic -> httpBasic.disable()) + + // 경로별 인가 설정 + .authorizeRequests(auth -> auth + .requestMatchers("/auth", "/").permitAll() + .requestMatchers("/api/post/info/**").permitAll() + .requestMatchers("/api/post/cheongtakju/**").permitAll() + .requestMatchers("/api/post/fruitWine/**").permitAll() + .requestMatchers("/api/post/all/**").permitAll() + .requestMatchers("/api/post/search").permitAll() + .requestMatchers("/auth/reissue").permitAll() + .requestMatchers("/auth/signup","/auth/login","/auth/logout","/error").permitAll() + .requestMatchers("/images/**").permitAll() + .requestMatchers("/api/post/comments/**").permitAll() + .anyRequest().authenticated() // 그 외 경로는 인증 필요 + ) + // JWT 필터 등록 + .addFilterAt(new LoginFilter(authenticationManager(http), jwtUtil, refreshRepository), UsernamePasswordAuthenticationFilter.class) + .addFilterAt(new JWTFilter(jwtUtil), LoginFilter.class) + .addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class) + + // 세션 설정: JWT 사용하므로 세션을 사용하지 않도록 설정 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + return http.build(); + } + // Cors 설정 + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://holjjak.netlify.app")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("Authorization","refresh","Content-Type")); + configuration.setExposedHeaders(Arrays.asList("Authorization", "refresh")); // 클라이언트가 읽을 수 있는 응답 헤더 + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} + diff --git a/Backend/src/main/java/com/web3/Backend/config/SwaggerConfig.java b/Backend/src/main/java/com/web3/Backend/config/SwaggerConfig.java new file mode 100644 index 0000000..3e3f8f5 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package com.web3.Backend.config; + + + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.responses.ApiResponse; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .components(new Components() + .addResponses("defaultApiResponse", new ApiResponse() + .description("API 요청이 성공적으로 처리된 경우 반환되는 응답입니다.")) + .addResponses("errorApiResponse", new ApiResponse() + .description("API 요청 처리 중 에러가 발생한 경우 반환되는 응답입니다. 에러 코드와 메시지가 포함됩니다."))); + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/controller/LoginController.java b/Backend/src/main/java/com/web3/Backend/controller/LoginController.java new file mode 100644 index 0000000..11c7457 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/controller/LoginController.java @@ -0,0 +1,86 @@ +package com.web3.Backend.controller; + +import com.web3.Backend.domain.RefreshEntity; +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.dto.UserDto; +import com.web3.Backend.jwt.JWTUtil; +import com.web3.Backend.repository.RefreshRepository; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.validation.BindingResult; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.Date; + +@RestController +public class LoginController { + + private final AuthenticationManager authenticationManager; + private final JWTUtil jwtUtil; + private final RefreshRepository refreshRepository; + + public LoginController(AuthenticationManager authenticationManager, JWTUtil jwtUtil, RefreshRepository refreshRepository) { + this.authenticationManager = authenticationManager; + this.jwtUtil = jwtUtil; + this.refreshRepository = refreshRepository; + } + + @PostMapping("/auth/login") + public ResponseEntity login(@RequestBody @Validated UserDto userDto, BindingResult bindingResult, HttpServletResponse response) { + + if(bindingResult.hasErrors()){ + //유효성 검사 실패 시, 오류 메시지 반환 + String errorMessage = bindingResult.getFieldError().getDefaultMessage(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("잘못된 입력: "+errorMessage); + } + + String username = userDto.getUserName(); + String password = userDto.getPassword(); + + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password); + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + int userId = customUserDetails.getUser().getId(); // UserDetails에서 userId를 추출 + + RefreshEntity existingRefreshToken = refreshRepository.findByUsername(username); + + String accessToken = jwtUtil.createJwt("access", username, 600000L, userId); + String refreshToken; + + if(existingRefreshToken != null) { + //기존 refresh token이 있다면, 만료되었는지 확인 + if(jwtUtil.isExpired(existingRefreshToken.getRefresh())){ + //refresh token이 만료되었으면 + refreshToken= jwtUtil.createJwt("refresh",username,8640000L, userId); + //기존의 만료된 refresh token을 삭제하고 새로 저장 + refreshRepository.delete(existingRefreshToken); + addRefreshEntity(username,refreshToken,8640000L); + }else{ + //refresh toekn이 만료되지 않았으면 기존의 refresh token을 사용 + refreshToken = existingRefreshToken.getRefresh(); + } + }else{ + //새로 refresh token 생성 + refreshToken = jwtUtil.createJwt("refresh", username, 86400000L, userId); + addRefreshEntity(username,refreshToken,86400000L); + } + // 응답 헤더에 access token 과 refresh token을 설정 + response.setHeader("Authorization", "Bearer " + accessToken); + response.setHeader("refresh", refreshToken); + + return ResponseEntity.status(HttpServletResponse.SC_OK).body("login successful"); + } + private void addRefreshEntity(String username, String refresh, Long expiredMs) { + Date expiration = new Date(System.currentTimeMillis() + expiredMs); + RefreshEntity refreshEntity = new RefreshEntity(username, refresh, expiration.toString()); + refreshRepository.save(refreshEntity); + } +} diff --git a/Backend/src/main/java/com/web3/Backend/controller/LogoutController.java b/Backend/src/main/java/com/web3/Backend/controller/LogoutController.java new file mode 100644 index 0000000..1840152 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/controller/LogoutController.java @@ -0,0 +1,47 @@ +package com.web3.Backend.controller; + +import com.web3.Backend.domain.RefreshEntity; +import com.web3.Backend.jwt.JWTUtil; +import com.web3.Backend.repository.RefreshRepository; +import io.jsonwebtoken.ExpiredJwtException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class LogoutController { + private final JWTUtil jwtUtil; + private final RefreshRepository refreshRepository; + + public LogoutController(JWTUtil jwtUtil, RefreshRepository refreshRepository) { + this.jwtUtil = jwtUtil; + this.refreshRepository = refreshRepository; + } + + @PostMapping("/auth/logout") + public ResponseEntity logout(@RequestHeader(value = "Authorization") String authHeader) { + // Authorization 헤더에서 리프레시 토큰 추출 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return new ResponseEntity<>("No refresh token found", HttpStatus.BAD_REQUEST); + } + + String refreshToken = authHeader.substring(7); // "Bearer " 제거 + + // 리프레시 토큰 만료 체크 + if (jwtUtil.isExpired(refreshToken)) { + return new ResponseEntity<>("Refresh token expired", HttpStatus.BAD_REQUEST); + } + + // DB에서 리프레시 토큰 존재 여부 확인 + String username =jwtUtil.getUsername(refreshToken); + RefreshEntity refreshEntity = refreshRepository.findByUsername(username); + if (refreshEntity ==null) { + return new ResponseEntity<>("Refresh token not found", HttpStatus.BAD_REQUEST); + } + // DB에서 리프레시 토큰 삭제 + refreshRepository.delete(refreshEntity); + return new ResponseEntity<>("Successfully log out", HttpStatus.OK); + } +} diff --git a/Backend/src/main/java/com/web3/Backend/controller/PostController.java b/Backend/src/main/java/com/web3/Backend/controller/PostController.java new file mode 100644 index 0000000..9252001 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/controller/PostController.java @@ -0,0 +1,196 @@ +package com.web3.Backend.controller; + +import com.web3.Backend.domain.Comment; +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.dto.RatingDto; +import com.web3.Backend.exception.CustomException; +import com.web3.Backend.response.Response; +import com.web3.Backend.dto.PostDto; +import com.web3.Backend.service.PostService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +@RestController +@RequestMapping("/api") +public class PostController { + + @Autowired + private PostService postService; + + @GetMapping("/post/info/{postId}") + public ResponseEntity getPostInfo(@PathVariable int postId) { + PostDto postDto = postService.getPostById(postId); + + Map data = new HashMap<>(); + + data.put("postDto", postDto); + + + Response response = new Response("200", "게시물 정보 조회 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PutMapping("/post/bookmark/{id}") + public ResponseEntity clickBookmark(@PathVariable("id") int postId) { + // SecurityContext에서 인증된 사용자 정보를 CustomUserDetails로 직접 가져옴 + CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + // CustomUserDetails에서 필요한 정보를 바로 사용 + String username = customUserDetails.getUsername(); // username 사용 + String result = postService.clickBookmark(customUserDetails, postId); + Response response = new Response("200", result); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PostMapping("/post/rating/{postId}") + public ResponseEntity ratePost( + @PathVariable("postId") int postId, + @RequestBody RatingDto ratingDto) { + + CustomUserDetails customUserDetails = + (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + double updatedRating = postService.ratePost(customUserDetails, postId, ratingDto.getRating()); + + Response response = new Response("200", "별점 등록 성공", Map.of("rating", updatedRating)); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + //댓글 등록 + @PostMapping("/post/comment/{postId}") + public ResponseEntity addComment( + @PathVariable int postId, + @RequestBody Map requestBody) { + + CustomUserDetails customUserDetails = + (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + String content = requestBody.get("content"); + if (content == null || content.isBlank()) { + return new ResponseEntity<>(new Response("400", "내용을 입력해주세요.", null), HttpStatus.BAD_REQUEST); + } + + Comment comment = postService.addComment(customUserDetails, postId, content); + + // 응답 생성 + Map data = Map.of("content", comment.getContent()); + Response response = new Response("200", "댓글 등록 성공", data); + + return new ResponseEntity<>(response, HttpStatus.OK); + } + + //댓글 조회 + @GetMapping("/post/comments/{postId}") + public ResponseEntity getCommentsByPostId(@PathVariable int postId) { + Map data = postService.getCommentsDataByPostId(postId); + + Response response = new Response("200", "댓글 조회 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + // 전체 페이지 + @GetMapping("/post/all/{page}") + public ResponseEntity getAllPosts( + @PathVariable int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) List areas, + @RequestParam(required = false) String preferenceLevel + ) { + + try { + Page postPage = postService.getAllPosts(page, size, areas, preferenceLevel); + return buildResponse(postPage, "전체 데이터 조회 성공"); + } catch (IllegalArgumentException e) { + return buildErrorResponse(e.getMessage()); + } + } + + //청탁주 페이지 + @GetMapping("/post/cheongtakju/{page}") + public ResponseEntity getCheongTakjuPage( + @PathVariable int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) List areas, + @RequestParam(required = false) String preferenceLevel) { + + try { + Page postPage = postService.getCheongTakjuPage(page, size, areas, preferenceLevel); + return buildResponse(postPage, "청탁주 데이터 조회 성공"); + } catch (IllegalArgumentException e) { + return buildErrorResponse(e.getMessage()); + } + } + + //과실주 페이지 + @GetMapping("/post/fruitWine/{page}") + public ResponseEntity getFruitWinePage( + @PathVariable int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) List areas, + @RequestParam(required = false) String preferenceLevel) { + try { + Page postPage = postService.getFruitWinePage(page, size, areas, preferenceLevel); + return buildResponse(postPage, "과실주 데이터 조회 성공"); + } catch (IllegalArgumentException e) { + return buildErrorResponse(e.getMessage()); + } + } + + //공통 응답 생성(성공) + private ResponseEntity buildResponse(Page postPage, String successMessage) { + if (postPage.getContent().isEmpty()) { + Response response = new Response("404", "데이터가 없습니다.", null); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + Map data = new HashMap<>(); + data.put("content", postPage.getContent()); + data.put("totalPages", postPage.getTotalPages()); + data.put("currentPage", postPage.getNumber()); + + Response response = new Response("200", successMessage, data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + // 공통 응답 생성 (실패) + private ResponseEntity buildErrorResponse(String errorMessage) { + Response response = new Response("400", errorMessage, null); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + + //검색 기능 + @GetMapping("/post/search") + public ResponseEntity searchPost( + @RequestParam(value="drinkName",required = false) String drinkName, + @RequestParam(value="page",defaultValue="0") int page, + @RequestParam(value="size",defaultValue="10") int size) { + + Map data = new HashMap<>(); + + if (drinkName == null || drinkName.trim().isEmpty()) { + //빈 문자열 + Response response = new Response("400", "잘못된 접근", data); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + Page searchResult = postService.searchPostByName(drinkName, page, size); + if (searchResult.isEmpty()) { + Response response = new Response("404", "해당 데이터가 존재하지 않습니다.", data); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } else { + data.put("searchResult", searchResult); + Response response = new Response("200", "검색 결과 조회 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + } +} diff --git a/Backend/src/main/java/com/web3/Backend/controller/ReissueController.java b/Backend/src/main/java/com/web3/Backend/controller/ReissueController.java new file mode 100644 index 0000000..bbd813f --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/controller/ReissueController.java @@ -0,0 +1,79 @@ +package com.web3.Backend.controller; + +import com.web3.Backend.domain.RefreshEntity; +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.jwt.JWTUtil; +import com.web3.Backend.repository.RefreshRepository; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; + +@RestController +public class ReissueController { + + private final JWTUtil jwtUtil; + private final RefreshRepository refreshRepository; + + public ReissueController(JWTUtil jwtUtil, RefreshRepository refreshRepository) { + this.jwtUtil = jwtUtil; + this.refreshRepository = refreshRepository; + } + + @PostMapping("/auth/reissue") + public ResponseEntity reissue(@RequestHeader(value = "Authorization", required = true) String authHeader, HttpServletResponse response) { + + // 1. Authorization 헤더에서 "Bearer "을 제거하고, refresh token을 추출 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return new ResponseEntity<>("No refresh token found", HttpStatus.BAD_REQUEST); + } + + String refreshToken = authHeader.substring(7); // "Bearer " 제거 + + // 2. Refresh Token이 만료되었는지 확인 + if (jwtUtil.isExpired(refreshToken)) { + return new ResponseEntity<>("Refresh token expired", HttpStatus.BAD_REQUEST); // Refresh Token 만료 + } + + // 3. Refresh Token이 올바른지 확인 (Refresh Token은 "refresh" 카테고리여야 함) + String category = jwtUtil.getCategory(refreshToken); + if (!category.equals("refresh")) { + return new ResponseEntity<>("Invalid refresh token", HttpStatus.BAD_REQUEST); // 잘못된 토큰 + } + + // 4. DB에서 Refresh Token 확인 + String username = jwtUtil.getUsername(refreshToken); // Refresh Token에서 사용자 이름 얻기 + RefreshEntity refreshEntity = refreshRepository.findByUsername(username); + if (refreshEntity == null) { + return new ResponseEntity<>("Invalid refresh token", HttpStatus.BAD_REQUEST); // DB에 존재하지 않는 사용자 + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + int userId = customUserDetails.getUserId(); // CustomUserDetails에서 userId 추출 + + // 5. 새로운 Access Token과 Refresh Token 발급 + String newAccessToken = jwtUtil.createJwt("access", username, 600000L, userId); // 새로운 Access Token 생성 + String newRefreshToken = jwtUtil.createJwt("refresh", username, 86400000L, userId); // 새로운 Refresh Token 생성 + + // 6. 기존의 Refresh Token 삭제 후 새로운 Refresh Token을 DB에 저장 + refreshRepository.delete(refreshEntity); // 기존 Refresh Token 삭제 + addRefreshEntity(username, newRefreshToken, 86400000L); // 새로운 Refresh Token 저장 + // 7. 새로운 토큰들을 응답 헤더에 설정 + response.setHeader("Authorization", "Bearer " + newAccessToken); + response.setHeader("refresh", newRefreshToken); + return new ResponseEntity<>("New tokens issued", HttpStatus.OK); // 성공적인 응답 + } + // RefreshEntity 추가 메서드 + private void addRefreshEntity(String username, String refresh, Long expiredMs) { + Date expiration = new Date(System.currentTimeMillis() + expiredMs); // 만료 시간 계산 + RefreshEntity refreshEntity = new RefreshEntity(username, refresh, expiration.toString()); // RefreshEntity 생성 + refreshRepository.save(refreshEntity); // DB에 저장 + } +} diff --git a/Backend/src/main/java/com/web3/Backend/controller/SignUpController.java b/Backend/src/main/java/com/web3/Backend/controller/SignUpController.java new file mode 100644 index 0000000..01b7c54 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/controller/SignUpController.java @@ -0,0 +1,41 @@ +package com.web3.Backend.controller; + +import com.web3.Backend.dto.UserDto; +import com.web3.Backend.service.SignUpService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SignUpController { + private final SignUpService signupService; + + public SignUpController(SignUpService signupService) { + this.signupService = signupService; + } + + @PostMapping("/auth/signup") + public ResponseEntity signUp(@RequestBody UserDto userDto, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + // 유효성 검사가 실패했을 경우 + StringBuilder errors = new StringBuilder(); + bindingResult.getAllErrors().forEach(error -> errors.append(error.getDefaultMessage()).append("\n")); + return new ResponseEntity<>(errors.toString(), HttpStatus.BAD_REQUEST); + } + + try { + signupService.SignUpProcess(userDto); // 회원가입 처리 + return new ResponseEntity<>("User successfully created", HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + // 유효성 검사 실패 시 400 Bad Request 반환 + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } catch (Exception e) { + // 예기치 않은 오류가 발생했을 때 500 Internal Server Error 반환 + return new ResponseEntity<>("An error occurred while processing the request", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/controller/UserController.java b/Backend/src/main/java/com/web3/Backend/controller/UserController.java new file mode 100644 index 0000000..c557702 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/controller/UserController.java @@ -0,0 +1,140 @@ +package com.web3.Backend.controller; + +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.dto.PostPreviewDto; +import com.web3.Backend.dto.UserDto; +import com.web3.Backend.exception.CustomException; +import com.web3.Backend.exception.ErrorCode; +import com.web3.Backend.response.Response; +import com.web3.Backend.security.CurrentUser; +import com.web3.Backend.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/mypage/info") + public ResponseEntity getUserInfo(@CurrentUser CustomUserDetails currentUser) { + // 사용자 인증 확인 + if (currentUser == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + int userId = currentUser.getUser().getId(); + UserDto userDto = userService.getUserById(userId); + + Map data = new LinkedHashMap<>(); + Map userMap = new LinkedHashMap<>(); + + // 순서대로 넣어줌 + userMap.put("userId", userDto.getUserId()); + userMap.put("userName", userDto.getUserName()); + userMap.put("profileImage", userDto.getProfileImageUrl()); + userMap.put("preferenceLevel", userDto.getPreferenceLevel()); + + data.put("userDto", userMap); + + Response response = new Response("200", "내 정보 조회 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @GetMapping("/mypage/bookmarks") + public ResponseEntity getBookmarks(@CurrentUser CustomUserDetails customUserDetails) { + if (customUserDetails == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + List postPreviewDtos = userService.getBookmarks(customUserDetails.getUser().getId()); + + Map data = new HashMap<>(); + data.put("postPreviewDtos", postPreviewDtos); + + Response response = new Response("200", "북마크 목록 조회 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PatchMapping("/mypage/updateInfo") + public ResponseEntity updateUserInfo(@CurrentUser CustomUserDetails customUserDetails, @RequestBody Map request) { + if (customUserDetails == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + int userId = customUserDetails.getUser().getId(); // 현재 사용자의 ID + String newUserId = (String) request.get("userId"); // 요청에서 새로운 userId 가져오기 + + // userId 변경 + UserDto updatedUser = userService.updateUserId(userId, newUserId); + + Map data = new HashMap<>(); + data.put("userId", updatedUser.getUserId()); + + Response response = new Response("200", "아이디 수정 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PatchMapping("/mypage/updatePreference") + public ResponseEntity updatePreference(@CurrentUser CustomUserDetails customUserDetails, @RequestBody Map request) { + if (customUserDetails == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + Double preferenceLevel; + try { + preferenceLevel = ((Number) request.get("preferenceLevel")).doubleValue(); + } catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_PREFERENCE_LEVEL); + } + + // 선호도 수정 + userService.updatePreferenceLevel(customUserDetails.getUser().getId(), preferenceLevel); + + // 응답 생성 + Map data = new HashMap<>(); + data.put("preferenceLevel", preferenceLevel); + + Response response = new Response("200", "선호 도수 설정 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PatchMapping("/mypage/updateProfileImage") + public ResponseEntity updateProfileImage( + @CurrentUser CustomUserDetails customUserDetails, + @RequestParam("profileImage") MultipartFile profileImage) { + + if (customUserDetails == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + if (profileImage.isEmpty() || !isValidImageFormat(profileImage.getOriginalFilename())) { + throw new CustomException(ErrorCode.INVALID_IMAGE_FORMAT); + } + + // 프로필 이미지 업데이트 + String savedImageUrl = userService.updateProfileImage(customUserDetails.getUser().getId(), profileImage); + + Map data = new HashMap<>(); + data.put("profileImageUrl", savedImageUrl); + + Response response = new Response("200", "프로필 사진 설정 성공", data); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + // 이미지 파일 형식 검사 + private boolean isValidImageFormat(String filename) { + return filename.matches(".*\\.(jpeg|jpg|png|gif|bmp)$"); + } +} + diff --git a/Backend/src/main/java/com/web3/Backend/domain/Bookmark.java b/Backend/src/main/java/com/web3/Backend/domain/Bookmark.java new file mode 100644 index 0000000..9ba308f --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/domain/Bookmark.java @@ -0,0 +1,22 @@ +package com.web3.Backend.domain; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "bookmark") +public class Bookmark { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int tableId; + + @ManyToOne + @JoinColumn(name = "postId", referencedColumnName = "postId") + private Post post; + + @ManyToOne + @JoinColumn(name = "id", referencedColumnName = "id") + private User user; +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/domain/Comment.java b/Backend/src/main/java/com/web3/Backend/domain/Comment.java new file mode 100644 index 0000000..0050796 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/domain/Comment.java @@ -0,0 +1,31 @@ +package com.web3.Backend.domain; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "comment") +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT 설정 + @Column(name = "commentId") // 데이터베이스 필드와 매핑 + private Long commentId; // 기본 키 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; // 댓글이 속한 게시물 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; // 댓글을 작성한 사용자 + + @Column(nullable = false, length = 500) + private String content; // 댓글 내용 + +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/domain/Post.java b/Backend/src/main/java/com/web3/Backend/domain/Post.java new file mode 100644 index 0000000..4511ad1 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/domain/Post.java @@ -0,0 +1,30 @@ +package com.web3.Backend.domain; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "post") +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "postId") + private int postId; + + private String drinkName; + private Double preferenceLevel; + private String postImage; + private String type; + private String area; + private String food; + + @Column(nullable = false) + private Double rating = 0.0; + + @Column(nullable = false) + private int ratingCount = 0; // 별점 개수 + +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/domain/Rating.java b/Backend/src/main/java/com/web3/Backend/domain/Rating.java new file mode 100644 index 0000000..50aadaf --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/domain/Rating.java @@ -0,0 +1,28 @@ +package com.web3.Backend.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "rating") +public class Rating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT 설정 + @Column(name = "ratingId") // 데이터베이스 필드와 매핑 + private Long ratingId; // 기본 키 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; // 별점이 속한 게시물 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; // 별점을 남긴 사용자 + + @Column(nullable = false) + private Double ratingValue; // 사용자 별점 +} diff --git a/Backend/src/main/java/com/web3/Backend/domain/RefreshEntity.java b/Backend/src/main/java/com/web3/Backend/domain/RefreshEntity.java new file mode 100644 index 0000000..9ae842e --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/domain/RefreshEntity.java @@ -0,0 +1,27 @@ +package com.web3.Backend.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name="Refresh") +public class RefreshEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + private String refresh; + private String expiration; + //기본 생성자 추가 + public RefreshEntity(){ + } + public RefreshEntity(String username, String refresh, String expiration) { + this.username = username; + this.refresh = refresh; + this.expiration = expiration; + } +} diff --git a/Backend/src/main/java/com/web3/Backend/domain/User.java b/Backend/src/main/java/com/web3/Backend/domain/User.java new file mode 100644 index 0000000..22dffcf --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/domain/User.java @@ -0,0 +1,26 @@ +package com.web3.Backend.domain; +import jakarta.persistence.*; +import lombok.*; + + +@Entity +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "user") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + @Column(nullable=false,unique = true,length=255) + private String userName; + @Column(unique=true,nullable=false,length=50) + private String userId; + @Column(nullable=false,length=255) + private String password; + private Double preferenceLevel; + private String profileImageUrl; + @Column(nullable = true) + private String role; +} diff --git a/Backend/src/main/java/com/web3/Backend/dto/CustomUserDetails.java b/Backend/src/main/java/com/web3/Backend/dto/CustomUserDetails.java new file mode 100644 index 0000000..41032e1 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/dto/CustomUserDetails.java @@ -0,0 +1,67 @@ +package com.web3.Backend.dto; + +import com.web3.Backend.domain.User; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class CustomUserDetails implements UserDetails { + // User 객체 반환 + @Getter + private final User user; + private String username; + private String password; + private List authorities; + + public CustomUserDetails(User user) { + this.user = user; + this.username = user.getUserName(); + this.password = user.getPassword(); + this.authorities = Collections.emptyList(); //권한을 빈 리스트로 설정 + } + + @Override + public String getUsername() { + return user.getUserName(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public Collection getAuthorities() { + // 빈 리스트를 반환하여 권한이 없음을 나타냄 + return authorities; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + public int getUserId() { + return user.getId(); // User 객체에서 userId를 반환 + } +} diff --git a/Backend/src/main/java/com/web3/Backend/dto/PostDto.java b/Backend/src/main/java/com/web3/Backend/dto/PostDto.java new file mode 100644 index 0000000..b179848 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/dto/PostDto.java @@ -0,0 +1,24 @@ +package com.web3.Backend.dto; + +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +public class PostDto { + private int postId; + private String drinkName; + private Double preferenceLevel; + private String postImage; + private String type; + private String area; + private Double rating; // 평균 평점 + + + public PostDto(){} + + public Double getRating() { + //rating을 0.0으로 기본값을 설정 + return rating == null ? 0.0 : Math.round(rating * 2 ) / 2.0; + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/dto/PostPreviewDto.java b/Backend/src/main/java/com/web3/Backend/dto/PostPreviewDto.java new file mode 100644 index 0000000..0e32482 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/dto/PostPreviewDto.java @@ -0,0 +1,14 @@ +package com.web3.Backend.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class PostPreviewDto { + private int postId; + private String postImage; +} diff --git a/Backend/src/main/java/com/web3/Backend/dto/RatingDto.java b/Backend/src/main/java/com/web3/Backend/dto/RatingDto.java new file mode 100644 index 0000000..382f1ed --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/dto/RatingDto.java @@ -0,0 +1,10 @@ +package com.web3.Backend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RatingDto { + private double rating; // 별점 값 +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/dto/UserDto.java b/Backend/src/main/java/com/web3/Backend/dto/UserDto.java new file mode 100644 index 0000000..ce4a385 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/dto/UserDto.java @@ -0,0 +1,33 @@ +package com.web3.Backend.dto; + +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserDto { + // 사용자명 정규식 : 2~20자의 영어, 숫자만 허용, 반드시 @포함 + @NotBlank + @Pattern(regexp = "^[a-zA-Z0-9]{2,10}@[a-zA-Z0-9]{2,20}$",message ="userName은 2~10자의 영문자 또는 숫자와 '@' 기호를 포함하고, '@' 뒤에는 2~20자의 영문자 또는 숫자가 포함되어야 합니다.") + private String userName; + // 사용자 아이디 정규식 : 2~10자의 영어, 한글, 숫자만 허용 + @Pattern(regexp = "^[a-zA-zㄱ-ㅎ가-힣]{2,10}$",message="사용자 이름은 2~10자여야 하며 문자와 한국어 문자만 포함해야 합니다.") + private String userId; + // 패스워드 정규식 : 최소 8자 이상, 대소문자, 숫자, 특수문자 포함 + @NotBlank + @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@$!%*?&])[A-Za-z0-9@$!%*?&].{8,16}$",message="비밀번호는 숫자 1개, 문자 1개, 특수 문자 1개 이상을 포함하여 8~16자여야 합니다.") + private String password; + @Null + private Double preferenceLevel; + @Null + private String profileImageUrl; + @Null + private String role; +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/exception/CustomException.java b/Backend/src/main/java/com/web3/Backend/exception/CustomException.java new file mode 100644 index 0000000..f1b6155 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/exception/CustomException.java @@ -0,0 +1,14 @@ +package com.web3.Backend.exception; + +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException { + + private final ErrorCode errorCode; + + public CustomException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/exception/ErrorCode.java b/Backend/src/main/java/com/web3/Backend/exception/ErrorCode.java new file mode 100644 index 0000000..f7e774f --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/exception/ErrorCode.java @@ -0,0 +1,43 @@ +package com.web3.Backend.exception; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + BAD_REQUEST(HttpStatus.BAD_REQUEST, "400", "잘못된 요청입니다."),//GlobalExceptionHandler에서 처리 + INVALID_POST_ID(HttpStatus.BAD_REQUEST, "400-1", "잘못된 게시물 ID입니다."), + INVALID_PATH_VARIABLE(HttpStatus.BAD_REQUEST, "400-2", "잘못된 경로 변수입니다."),//GlobalExceptionHandler에서 처리 + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "401", "인증되지 않은 사용자입니다."), //GlobalExceptionHandler에서 처리 + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "401-1", "비밀번호가 일치하지 않습니다."), + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "404", "해당 ID에 대한 게시물을 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "404-1", "해당 사용자를 찾을 수 없습니다."), + USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "409", "사용자가 이미 존재합니다."), + DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "데이터베이스 처리 중 오류가 발생했습니다."), + INVALID_PREFERENCE_LEVEL(HttpStatus.BAD_REQUEST, "400-3", "유효하지 않은 도수 값입니다."),// 음수거나 범위에서 벗어난 경우 + INVALID_IMAGE_FORMAT(HttpStatus.BAD_REQUEST, "400-4", "유효하지 않은 이미지 형식입니다."), + INVALID_RATING_VALUE(HttpStatus.BAD_REQUEST, "400-5", "유효하지 않은 별점 값입니다."), + + USER_ID_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "400-6", "이미 존재하는 아이디입니다."); // 아이디 중복 오류 + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + ErrorCode(HttpStatus httpStatus, String code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/exception/GlobalExceptionHandler.java b/Backend/src/main/java/com/web3/Backend/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..54f5de4 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/exception/GlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package com.web3.Backend.exception; + +import com.web3.Backend.response.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(CustomException.class) + public ResponseEntity handleCustomException(CustomException ex) { + ErrorCode errorCode = ex.getErrorCode(); + + Response response = new Response(errorCode.getCode(), errorCode.getMessage(), null); + return new ResponseEntity<>(response, errorCode.getHttpStatus()); + } + + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) { + Response response = new Response("400-2", "잘못된 경로 변수입니다.", null); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + Response response = new Response("401", "인증되지 않은 사용자입니다.", null); + return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleBadRequest(Exception ex) { + Response response = new Response("400", "잘못된 요청입니다.", null); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleServerError(Exception ex) { + Response response = new Response("500", "서버 내부 오류입니다.", null); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/jwt/CustomLogoutFilter.java b/Backend/src/main/java/com/web3/Backend/jwt/CustomLogoutFilter.java new file mode 100644 index 0000000..cec364c --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/jwt/CustomLogoutFilter.java @@ -0,0 +1,67 @@ +package com.web3.Backend.jwt; + +import com.web3.Backend.repository.RefreshRepository; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +public class CustomLogoutFilter extends GenericFilterBean { + private final JWTUtil jwtUtil; + private final RefreshRepository refreshRepository; + + public CustomLogoutFilter(JWTUtil jwtUtil, RefreshRepository refreshRepository) { + this.jwtUtil = jwtUtil; + this.refreshRepository = refreshRepository; + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); + } + + private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + String requestUri = request.getRequestURI(); + + // '/logout' 요청이 들어오면 필터가 이를 처리 + if ("/auth/logout".equals(requestUri)) { + String authHeader = request.getHeader("Authorization"); + + // Authorization 헤더에서 리프레시 토큰을 추출 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write("No refresh token found"); + return; + } + String refreshToken = authHeader.substring(7); // "Bearer " 제거 + + // 리프레시 토큰 만료 체크 + try { + jwtUtil.isExpired(refreshToken); + } catch (ExpiredJwtException e) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write("Refresh token expired"); + return; + } + + // 리프레시 토큰이 DB에 존재하는지 확인 + if (!refreshRepository.existsByRefresh(refreshToken)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write("Invalid refresh token"); + return; + } + // DB에서 리프레시 토큰 삭제 + refreshRepository.deleteByRefresh(refreshToken); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("Successfully logged out"); + } else { + // 다른 요청은 필터를 계속 진행 + filterChain.doFilter(request, response); + } + } +} diff --git a/Backend/src/main/java/com/web3/Backend/jwt/JWTFilter.java b/Backend/src/main/java/com/web3/Backend/jwt/JWTFilter.java new file mode 100644 index 0000000..e6b4282 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/jwt/JWTFilter.java @@ -0,0 +1,76 @@ +package com.web3.Backend.jwt; + +import com.web3.Backend.domain.User; +import com.web3.Backend.dto.CustomUserDetails; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.io.PrintWriter; + +public class JWTFilter extends OncePerRequestFilter { + private final JWTUtil jwtUtil; + public JWTFilter(JWTUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if(request.getRequestURI().startsWith("/auth/signup") || + request.getRequestURI().startsWith("/api/post/all")|| + request.getRequestURI().startsWith("/api/post/cheongtakju") || + request.getRequestURI().startsWith("/api/post/fruitWine") || + request.getRequestURI().startsWith("/api/post/search")) { + + filterChain.doFilter(request,response); + return; + } + // 헤더에서 access키에 담긴 토큰을 꺼냄 + String accessToken = request.getHeader("Authorization"); + + //토큰이 없다면 다음 필터로 넘김 + if (accessToken != null && accessToken.startsWith("Bearer ")) { + accessToken = accessToken.substring(7);// "Bearer " 접두사를 제거하여 토큰만 추출 + if (jwtUtil.isExpired(accessToken)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Access token expired"); + return; + } + + // 토큰이 access인지 확인 (발급시 페이로드에 명시) + String category = jwtUtil.getCategory(accessToken); + + if (!category.equals("access")) { + //response body + PrintWriter writer = response.getWriter(); + writer.print("invalid access token"); + + //response status code + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + // username 값을 획득 + String username = jwtUtil.getUsername(accessToken); + // JWT에서 userId도 추출하여 설정하는 로직 추가 + int userId = jwtUtil.getUserId(accessToken); // getUserId는 jwtUtil에서 구현 필요 + + // User 객체 생성 후, username과 userId 설정 + User user = new User(); + user.setUserName(username); + user.setId(userId); // userId 설정 + + // CustomUserDetails 생성 + CustomUserDetails customUserDetails = new CustomUserDetails(user); + + // Authentication 생성 및 SecurityContext에 설정 + Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/jwt/JWTUtil.java b/Backend/src/main/java/com/web3/Backend/jwt/JWTUtil.java new file mode 100644 index 0000000..d5af89f --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/jwt/JWTUtil.java @@ -0,0 +1,63 @@ +package com.web3.Backend.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class JWTUtil { + private SecretKey secretKey; + + public JWTUtil(@Value("${spring_jwt_secret}")String secret) { + this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + } + + public String getUsername(String token) { + JwtParser parser = Jwts.parser() + .setSigningKey(secretKey) + .build(); + Claims claims =parser.parseClaimsJws(token).getBody(); + return claims.get("username",String.class); + + } + public String getCategory(String token) { + JwtParser parser = Jwts.parser() + .setSigningKey(secretKey) + .build(); + Claims claims = parser.parseClaimsJws(token).getBody(); + return claims.get("category",String.class); + } + + public int getUserId(String token) { + JwtParser parser = Jwts.parser().setSigningKey(secretKey).build(); + Claims claims = parser.parseClaimsJws(token).getBody(); + return claims.get("userId", Integer.class); // JWT에서 userId를 추출 + } + + public Boolean isExpired(String token) { + JwtParser parser = Jwts.parser() + .setSigningKey(secretKey) + .build(); + Claims claims = parser.parseClaimsJws(token).getBody(); + Date expiration = claims.getExpiration(); + return expiration.before(new Date()); + } + + public String createJwt(String category, String username, Long expiredMs, int userId) { + return Jwts.builder() + .claim("category", category) + .claim("username", username) + .claim("userId", userId) // userId를 JWT에 포함 + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/jwt/LoginFilter.java b/Backend/src/main/java/com/web3/Backend/jwt/LoginFilter.java new file mode 100644 index 0000000..007753e --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/jwt/LoginFilter.java @@ -0,0 +1,98 @@ +package com.web3.Backend.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.web3.Backend.domain.RefreshEntity; +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.dto.UserDto; +import com.web3.Backend.repository.RefreshRepository; +import io.jsonwebtoken.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import java.util.Date; + +@Slf4j +public class LoginFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JWTUtil jwtUtil; + private final RefreshRepository refreshRepository; + + public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil, RefreshRepository refreshRepository) { + this.authenticationManager = authenticationManager; + this.jwtUtil = jwtUtil; + this.refreshRepository = refreshRepository; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + // 요청 본문에서 username과 password를 JSON 형식으로 추출 + try { + // ObjectMapper를 사용하여 JSON 요청을 UserDto 객체로 변환 + ObjectMapper objectMapper = new ObjectMapper(); + UserDto userDto = objectMapper.readValue(request.getInputStream(), UserDto.class); + String username = userDto.getUserName(); // UserDto에서 userName을 가져옴 + String password = userDto.getPassword(); // UserDto에서 password를 가져옴 + log.info("Attempting authentication for username: {}, password: {}", username, password); + // username 또는 password가 없으면 예외를 던짐 + if (username == null || password == null) { + throw new BadCredentialsException("Username or password not provided"); + } + // 인증 토큰 생성 후 인증 시도 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + return authenticationManager.authenticate(authToken); // AuthenticationManager로 인증 시도 + } catch (IOException | java.io.IOException e) { + // JSON 파싱 에러 발생 시 예외 처리 + throw new BadCredentialsException("Invalid request format", e); + } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) + throws IOException, ServletException, java.io.IOException { + log.info("Authentication successful for user: " + authentication.getName()); + String username = authentication.getName(); + try { + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + int userId = customUserDetails.getUser().getId(); // CustomUserDetails에서 userId를 가져옴 + // Access Token과 Refresh Token 생성 + String accessToken = jwtUtil.createJwt("access", username, 600000L, userId); + String refreshToken = jwtUtil.createJwt("refresh", username, 86400000L, userId); + log.info("Access Token: {}", accessToken); + log.info("Refresh Token: {}", refreshToken); + // DB에 Refresh Token 저장 + addRefreshEntity(username, refreshToken, 86400000L); + // 생성된 JWT 토큰을 응답 헤더에 추가 + response.setHeader("Authorization", "Bearer " + accessToken); // Access Token을 Authorization 헤더에 추가 + response.setHeader("refresh", refreshToken); // Refresh Token을 refresh 헤더에 추가 + response.setStatus(HttpServletResponse.SC_OK); // HTTP 200 OK 상태 코드 반환 + } catch (Exception e) { + log.error("Error creating JWT tokens: ", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write("Error occurred while generating JWT tokens"); + } + } + + // Refresh Token을 DB에 저장하는 메서드 + @Transactional + private void addRefreshEntity(String username, String refresh, Long expiredMs) { + try { + Date expiration = new Date(System.currentTimeMillis() + expiredMs); // Refresh Token 만료 시간 계산 + RefreshEntity refreshEntity = new RefreshEntity(username, refresh, expiration.toString()); + refreshRepository.save(refreshEntity); // DB에 Refresh Token 저장 + log.info("Refresh token saved for username: {}", username); + } catch (Exception e) { + log.error("Error saving refresh token: ", e); + } + } +} + diff --git a/Backend/src/main/java/com/web3/Backend/repository/BookmarkRepository.java b/Backend/src/main/java/com/web3/Backend/repository/BookmarkRepository.java new file mode 100644 index 0000000..7b9256f --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/repository/BookmarkRepository.java @@ -0,0 +1,16 @@ +package com.web3.Backend.repository; +import com.web3.Backend.domain.Bookmark; +import com.web3.Backend.domain.Post; +import com.web3.Backend.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Repository +public interface BookmarkRepository extends JpaRepository { + // 사용자와 게시물에 해당하는 북마크를 찾는 메서드 + Bookmark findByUserAndPost(User user, Post post); + List findByUserId(int userId); +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/repository/CommentRepository.java b/Backend/src/main/java/com/web3/Backend/repository/CommentRepository.java new file mode 100644 index 0000000..93d6615 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/repository/CommentRepository.java @@ -0,0 +1,11 @@ +package com.web3.Backend.repository; + +import com.web3.Backend.domain.Comment; +import com.web3.Backend.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByPost(Post post); +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/repository/PostRepository.java b/Backend/src/main/java/com/web3/Backend/repository/PostRepository.java new file mode 100644 index 0000000..90334bc --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/repository/PostRepository.java @@ -0,0 +1,36 @@ +package com.web3.Backend.repository; +import com.web3.Backend.domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PostRepository extends JpaRepository { + // type이 청주 또는 탁주인 데이터에서, 선택된 지역과 preferenceLevel을 적용하여 조회 + Page findByTypeInAndAreaInAndPreferenceLevelBetween(List types, List areas, Double min, Double max, Pageable pageable); + // type이 청주 또는 탁주인 데이터에서, 지역 조건만 적용하여 조회 + Page findByTypeInAndAreaIn(List types, List areas, Pageable pageable); + // type이 청주 또는 탁주인 데이터만 조회 + Page findByTypeIn(List types, Pageable pageable); + // 과실주 데이터 조회 + Page findByTypeAndAreaInAndPreferenceLevelBetween(String type, List areas, Double min, Double max, Pageable pageable); + // 과실주 데이터에서 지역만 적용하여 조회 + Page findByTypeAndAreaIn(String type, List areas, Pageable pageable); + // 과실주만 조회 + Page findByType(String type, Pageable pageable); + // 지역과 preferenceLevel 범위에 맞는 데이터 조회 + Page findByAreaInAndPreferenceLevelBetween(List areas, Double min, Double max, Pageable pageable); + // 지역만 적용하여 데이터 조회 + Page findByAreaIn(List areas, Pageable pageable); + // 모든 데이터 조회 + Page findAll(Pageable pageable); + Page findByPreferenceLevelBetween(Double min, Double max, Pageable pageable); + Page findByTypeAndPreferenceLevelBetween(String type, Double min, Double max, Pageable pageable); + Page findByTypeInAndPreferenceLevelBetween(List types, Double min, Double max, Pageable pageable); + + Page findByDrinkNameContainingIgnoreCase(String drinkName, Pageable pageable); +} + diff --git a/Backend/src/main/java/com/web3/Backend/repository/RatingRepository.java b/Backend/src/main/java/com/web3/Backend/repository/RatingRepository.java new file mode 100644 index 0000000..ec22639 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/repository/RatingRepository.java @@ -0,0 +1,11 @@ +package com.web3.Backend.repository; + +import com.web3.Backend.domain.Rating; +import org.springframework.data.jpa.repository.JpaRepository; +import com.web3.Backend.domain.Post; +import com.web3.Backend.domain.User; + + +public interface RatingRepository extends JpaRepository { + Rating findByPostAndUser(Post post, User user); // 사용자와 게시물을 기반으로 별점 검색 +} diff --git a/Backend/src/main/java/com/web3/Backend/repository/RefreshRepository.java b/Backend/src/main/java/com/web3/Backend/repository/RefreshRepository.java new file mode 100644 index 0000000..3fa4758 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/repository/RefreshRepository.java @@ -0,0 +1,17 @@ +package com.web3.Backend.repository; + +import com.web3.Backend.domain.RefreshEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public interface RefreshRepository extends JpaRepository { + Boolean existsByRefresh(String refresh); + + //refresh token으로 사용자 찾기 + RefreshEntity findByUsername(String username); + + @Transactional + void deleteByRefresh(String refresh); +} diff --git a/Backend/src/main/java/com/web3/Backend/repository/UserRepository.java b/Backend/src/main/java/com/web3/Backend/repository/UserRepository.java new file mode 100644 index 0000000..3243e1a --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.web3.Backend.repository; +import com.web3.Backend.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + Boolean existsByUserName(String username); + User findByUserName(String username); + boolean existsByUserId(String userId); // userId가 존재하는지 확인하는 메서드 +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/response/Response.java b/Backend/src/main/java/com/web3/Backend/response/Response.java new file mode 100644 index 0000000..7462abb --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/response/Response.java @@ -0,0 +1,20 @@ +package com.web3.Backend.response; + + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class Response { + private String code; + private String message; + private Map data; + + public Response(String code, String message) { + this.code = code; + this.message = message; + this.data = null; + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/security/CurrentUser.java b/Backend/src/main/java/com/web3/Backend/security/CurrentUser.java new file mode 100644 index 0000000..b75cbc4 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/security/CurrentUser.java @@ -0,0 +1,11 @@ +package com.web3.Backend.security; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@AuthenticationPrincipal +public @interface CurrentUser { + // CustomUserDetails를 반환하는 어노테이션으로 사용 +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/service/CustomUserDetailsService.java b/Backend/src/main/java/com/web3/Backend/service/CustomUserDetailsService.java new file mode 100644 index 0000000..0bc389a --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/service/CustomUserDetailsService.java @@ -0,0 +1,26 @@ +package com.web3.Backend.service; + +import com.web3.Backend.domain.User; +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUserName(username); + if(user == null) { + throw new UsernameNotFoundException("User not found with username" + username); + } + return new CustomUserDetails(user); + } +} diff --git a/Backend/src/main/java/com/web3/Backend/service/FileStorageService.java b/Backend/src/main/java/com/web3/Backend/service/FileStorageService.java new file mode 100644 index 0000000..d3aa555 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/service/FileStorageService.java @@ -0,0 +1,47 @@ +package com.web3.Backend.service; + +import com.web3.Backend.exception.CustomException; +import com.web3.Backend.exception.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +@Service +public class FileStorageService { + + private final Path fileStorageLocation; + + + // application.properties에서 경로를 주입받음 + public FileStorageService(@Value("${file.upload-dir}") String uploadDir) { + this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize(); + + try { + Files.createDirectories(this.fileStorageLocation); + } catch (IOException e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } + + public String storeFile(MultipartFile file) { + // 파일 이름에 타임스탬프 추가하여 저장 + String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename(); + + try { + Path targetLocation = fileStorageLocation.resolve(fileName); + Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING); + + // 클라이언트에 반환할 URL 경로 생성 (예: /images/uploads/filename.jpg) + return "/images/uploads/" + fileName; + + } catch (IOException e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } +} diff --git a/Backend/src/main/java/com/web3/Backend/service/PostService.java b/Backend/src/main/java/com/web3/Backend/service/PostService.java new file mode 100644 index 0000000..b819b79 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/service/PostService.java @@ -0,0 +1,324 @@ +package com.web3.Backend.service; + +import com.web3.Backend.domain.*; +import com.web3.Backend.dto.CustomUserDetails; +import com.web3.Backend.dto.PostDto; +import com.web3.Backend.exception.CustomException; +import com.web3.Backend.exception.ErrorCode; +import com.web3.Backend.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + + +@Service +public class PostService { + @Autowired + private UserRepository userRepository; + @Autowired + private PostRepository postRepository; + @Autowired + private BookmarkRepository bookmarkRepository; + + @Autowired + private RatingRepository ratingRepository; + + @Autowired + private CommentRepository commentRepository; + public PostDto getPostById(int postId) { + try { + Optional postOptional = postRepository.findById(postId); + + if (postOptional.isPresent()) { + Post post = postOptional.get(); + PostDto postDto = new PostDto(); + postDto.setDrinkName(post.getDrinkName()); + postDto.setPreferenceLevel(post.getPreferenceLevel()); + postDto.setPostImage(post.getPostImage()); + postDto.setType(post.getType()); + postDto.setArea(post.getArea()); + postDto.setRating(post.getRating()); + + return postDto; + } else { + throw new CustomException(ErrorCode.POST_NOT_FOUND); + } + + } catch (NumberFormatException e) { + throw new CustomException(ErrorCode.INVALID_POST_ID); + } catch (Exception e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } + public String clickBookmark(CustomUserDetails customUserDetails, int postId) throws CustomException { + + int userId = customUserDetails.getUser().getId(); + System.out.println("User ID from SecurityContext: " + userId); // ID 확인 로그 추가 + + // 포스트 찾기 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + // 사용자 찾기 (CustomUserDetails에서 id 추출) + User user = userRepository.findById(Math.toIntExact(customUserDetails.getUser().getId())) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 북마크 엔트리 찾기 + Bookmark bookmarkEntry = bookmarkRepository.findByUserAndPost(user, post); + + // 북마크 삭제 또는 추가 + if (bookmarkEntry != null) { + bookmarkRepository.delete(bookmarkEntry); + return "북마크가 삭제되었습니다."; + } else { + Bookmark newBookmark = new Bookmark(); + newBookmark.setUser(user); + newBookmark.setPost(post); + try { + bookmarkRepository.save(newBookmark); + return "북마크가 추가되었습니다."; + } catch (Exception e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } + } + + + public double ratePost(CustomUserDetails customUserDetails, int postId, double ratingValue) { + // 1. 사용자 ID 추출 + int userId = customUserDetails.getUser().getId(); + + + if (ratingValue < 0.0 || ratingValue > 5.0) { + throw new CustomException(ErrorCode.INVALID_RATING_VALUE); + } + + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + try { + Rating existingRating = ratingRepository.findByPostAndUser(post, user); + + if (existingRating != null) { + // 기존 별점 수정 + double oldRating = existingRating.getRatingValue(); + existingRating.setRatingValue(ratingValue); + ratingRepository.save(existingRating); + + // 평균 별점 재계산 + post.setRating( + ((post.getRating() * post.getRatingCount()) - oldRating + ratingValue) + / post.getRatingCount() + ); + } else { + // 새 별점 등록 + Rating newRating = new Rating(); + newRating.setPost(post); + newRating.setUser(user); + newRating.setRatingValue(ratingValue); + ratingRepository.save(newRating); + + // 평균 별점 및 카운트 업데이트 + int newCount = post.getRatingCount() + 1; + post.setRating( + ((post.getRating() * post.getRatingCount()) + ratingValue) / newCount + ); + post.setRatingCount(newCount); + } + + double adjustedRating = Math.round(post.getRating() * 2) / 2.0; + postRepository.save(post); + + return adjustedRating; + } catch (Exception e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } + + public Comment addComment(CustomUserDetails customUserDetails, int postId, String content) { + + int userId = customUserDetails.getUser().getId(); + // 게시물 존재 여부 확인 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + + // 댓글 생성 및 저장 + Comment comment = Comment.builder() + .post(post) + .user(user) + .content(content) + .build(); + + commentRepository.save(comment); + + return commentRepository.save(comment); + } + + public Map getCommentsDataByPostId(int postId) { + // 게시물 존재 여부 확인 + Optional postOptional = postRepository.findById(postId); + if (postOptional.isEmpty()) { + throw new CustomException(ErrorCode.POST_NOT_FOUND); + } + + // 댓글 조회 + List comments = commentRepository.findByPost(postOptional.get()); + + // 댓글 데이터를 List> 형태로 변환 + List> commentData = comments.stream() + .map(comment -> { + Map map = new HashMap<>(); + map.put("commentId", comment.getCommentId()); + map.put("content", comment.getContent()); + map.put("postId", comment.getPost().getPostId()); + map.put("userId", comment.getUser().getUserId()); + return map; + }) + .collect(Collectors.toList()); + + // 데이터를 Map으로 감싸서 반환 + return Map.of("comments", commentData); + } + + //전체 페이지 + public Page getAllPosts(int page, int size, List areas, String preferenceLevel) { + Pageable pageable = PageRequest.of(page, size); + validateAreas(areas); + + Page postPage; + + if (preferenceLevel != null && areas != null && !areas.isEmpty()) { + Double[] range = getPreferenceLevelRange(preferenceLevel); + postPage = postRepository.findByAreaInAndPreferenceLevelBetween(areas, range[0], range[1], pageable); + } else if (areas != null && !areas.isEmpty()) { + postPage = postRepository.findByAreaIn(areas, pageable); + } else if (preferenceLevel != null) { + Double[] range = getPreferenceLevelRange(preferenceLevel); + postPage = postRepository.findByPreferenceLevelBetween(range[0], range[1], pageable); + } else { + postPage = postRepository.findAll(pageable); + } + + return postPage.map(this::mapToPostDto); + } + + // 청탁주 페이지 + public Page getCheongTakjuPage(int page, int size, List areas, String preferenceLevel) { + Pageable pageable = PageRequest.of(page, size); + validateAreas(areas); + + Page postPage; + + if (preferenceLevel != null && areas != null && !areas.isEmpty()) { + Double[] range = getPreferenceLevelRange(preferenceLevel); + postPage = postRepository.findByTypeInAndAreaInAndPreferenceLevelBetween( + List.of("청주", "탁주"), areas, range[0], range[1], pageable); + } else if (areas != null && !areas.isEmpty()) { + postPage = postRepository.findByTypeInAndAreaIn(List.of("청주", "탁주"), areas, pageable); + } else if (preferenceLevel != null) { + Double[] range = getPreferenceLevelRange(preferenceLevel); + postPage = postRepository.findByTypeInAndPreferenceLevelBetween(List.of("청주", "탁주"), range[0], range[1], pageable); + } else { + postPage = postRepository.findByTypeIn(List.of("청주", "탁주"), pageable); + } + + return postPage.map(this::mapToPostDto); + } + + // 과실주 페이지 + public Page getFruitWinePage(int page, int size, List areas, String preferenceLevel) { + Pageable pageable = PageRequest.of(page, size); + validateAreas(areas); + + Page postPage; + + if (preferenceLevel != null && areas != null && !areas.isEmpty()) { + Double[] range = getPreferenceLevelRange(preferenceLevel); + postPage = postRepository.findByTypeAndAreaInAndPreferenceLevelBetween( + "과실주", areas, range[0], range[1], pageable); + } else if (areas != null && !areas.isEmpty()) { + postPage = postRepository.findByTypeAndAreaIn("과실주", areas, pageable); + } else if (preferenceLevel != null) { + Double[] range = getPreferenceLevelRange(preferenceLevel); + postPage = postRepository.findByTypeAndPreferenceLevelBetween("과실주", range[0], range[1], pageable); + } else { + postPage = postRepository.findByType("과실주", pageable); + } + + return postPage.map(this::mapToPostDto); + } + + // 유효성 검사: 지역 + private void validateAreas(List areas) { + if (areas != null) { + List validAreas = List.of("서울특별시", "부산광역시", "대구광역시", "인천광역시", + "광주광역시", "대전광역시", "울산광역시", "세종특별자치시", "경기도", "강원도", "충청북도", + "충청남도", "전라북도", "전라남도", "경상북도", "경상남도", "제주특별자치도"); + + // 여러 지역이 들어오는 경우에도 잘 처리되도록 검증 + for (String area : areas) { + if (!validAreas.contains(area)) { + throw new IllegalArgumentException("잘못된 지역 값입니다: " + area); + } + } + } + } + + // Post -> PostDto 매핑 메서드 + private PostDto mapToPostDto(Post post) { + return PostDto.builder() + .postId(post.getPostId()) + .drinkName(post.getDrinkName()) + .preferenceLevel(post.getPreferenceLevel()) + .postImage("https://foreign-papagena-wap2024-2-web3-0d04a01a.koyeb.app" + post.getPostImage()) + .type(post.getType()) + .area(post.getArea()) + .rating(0.0) // 기본 rating 0.0 설정 + .build(); + } + + // preferenceLevel을 범위로 변환 + private Double[] getPreferenceLevelRange(String preferenceLevel) { + switch (preferenceLevel) { + case "0-5": + return new Double[]{0.0, 5.0}; + case "5-10": + return new Double[]{5.0, 10.0}; + case "10-15": + return new Double[]{10.0, 15.0}; + case "15-50": + return new Double[]{15.0, 50.0}; + default: + throw new IllegalArgumentException("잘못된 preferenceLevel 값입니다: " + preferenceLevel); + } + } + + //검색 기능을 페이징 처리 + public Page searchPostByName(String drinkName,int page,int size){ + PageRequest pageRequest = PageRequest.of(page,size); + return postRepository.findByDrinkNameContainingIgnoreCase(drinkName,pageRequest) + .map(post-> PostDto.builder() + .postId(post.getPostId()) + .drinkName(post.getDrinkName()) + .preferenceLevel(post.getPreferenceLevel()) + .postImage(post.getPostImage()) + .type(post.getType()) + .area(post.getArea()) + .rating(0.0) //rating을 사용하지 않으므로 기본값으로 설정 + .build()); + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/service/SignUpService.java b/Backend/src/main/java/com/web3/Backend/service/SignUpService.java new file mode 100644 index 0000000..2860a42 --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/service/SignUpService.java @@ -0,0 +1,58 @@ +package com.web3.Backend.service; + +import com.web3.Backend.domain.User; +import com.web3.Backend.dto.UserDto; +import com.web3.Backend.repository.UserRepository; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.regex.Pattern; + +@Service +public class SignUpService { + private final UserRepository userRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + public SignUpService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { + this.userRepository = userRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + } + + public void SignUpProcess(UserDto userDto) { + String username = userDto.getUserName(); + String password = userDto.getPassword(); + String userId = userDto.getUserId(); + + // 사용자명 정규식 검사 (UserDto에서 이미 처리됨) + if (!username.matches("^[a-zA-Z0-9]{2,10}@[a-zA-Z0-9]{2,20}$")) { + throw new IllegalArgumentException("userName must contain 2 to 10 alphanumeric characters followed by an '@' symbol, and the domain must contain 2 to 20 alphanumeric characters."); + } + + // 사용자 아이디 정규식 검사 (UserDto에서 이미 처리됨) + if (!userId.matches("^[a-zA-zㄱ-ㅎ가-힣]{2,10}$")) { + throw new IllegalArgumentException("userId must be between 2 and 10 characters, consisting of letters or Korean characters."); + } + + // 패스워드 정규식 검사 + String passwordPattern = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@$!%*?&])[A-Za-z0-9@$!%*?&].{8,16}$"; + Pattern pattern = Pattern.compile(passwordPattern); + if(!pattern.matcher(password).matches()){ + throw new IllegalArgumentException("password must be 8-16 characters, including at least one number, one letter, and one special character."); + } + if (userRepository.existsByUserName(username)) { + throw new IllegalArgumentException("User Name already exists"); + } + + //DB에 저장하기 위해 User 엔티티(domain) 로 변환 + User user = User.builder() + .userName(userDto.getUserName()) + .userId(userDto.getUserId()) + .password(bCryptPasswordEncoder.encode(userDto.getPassword())) + .preferenceLevel(null) + .profileImageUrl(null) + .role(null) + .build(); + // DB 에 저장 + userRepository.save(user); + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/service/SomeService.java b/Backend/src/main/java/com/web3/Backend/service/SomeService.java new file mode 100644 index 0000000..4f4280c --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/service/SomeService.java @@ -0,0 +1,25 @@ +package com.web3.Backend.service; +import com.web3.Backend.dto.CustomUserDetails; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.Authentication; + +public class SomeService { + public void someMethod() { + // SecurityContext에서 인증된 사용자 정보 가져오기 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 인증된 사용자가 있으면 + if (authentication != null) { + Object principal = authentication.getPrincipal(); // principal은 사용자 객체입니다. + + // principal이 CustomUserDetails인 경우에 사용자 정보 추출 + if (principal instanceof CustomUserDetails) { + CustomUserDetails userDetails = (CustomUserDetails) principal; + int userId = userDetails.getUser().getId(); // 예시: 사용자 ID 추출 + System.out.println("Authenticated User ID: " + userId); + } + } else { + System.out.println("No authenticated user found"); + } + } +} \ No newline at end of file diff --git a/Backend/src/main/java/com/web3/Backend/service/UserService.java b/Backend/src/main/java/com/web3/Backend/service/UserService.java new file mode 100644 index 0000000..55a847a --- /dev/null +++ b/Backend/src/main/java/com/web3/Backend/service/UserService.java @@ -0,0 +1,121 @@ +package com.web3.Backend.service; + + +import com.web3.Backend.domain.Bookmark; +import com.web3.Backend.domain.Post; +import com.web3.Backend.domain.User; +import com.web3.Backend.dto.PostPreviewDto; +import com.web3.Backend.dto.UserDto; +import com.web3.Backend.exception.CustomException; +import com.web3.Backend.exception.ErrorCode; +import com.web3.Backend.repository.BookmarkRepository; +import com.web3.Backend.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + @Autowired + private BookmarkRepository bookmarkRepository; + @Autowired + private FileStorageService fileStorageService; + + public UserDto getUserById(int userId) { + try { + Optional userOptional = userRepository.findById(userId); + if (userOptional.isPresent()) { + User user = userOptional.get(); + return UserDto.builder() + .userId(user.getUserId()) + .userName(user.getUserName()) + .profileImageUrl(user.getProfileImageUrl()) + .preferenceLevel(user.getPreferenceLevel()) + .build(); + } else { + throw new CustomException(ErrorCode.USER_NOT_FOUND); + } + + } catch (NumberFormatException e) { + throw new CustomException(ErrorCode.BAD_REQUEST); + } catch (Exception e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } + + public List getBookmarks(int userId) { + // 사용자 확인 + userRepository.findById(userId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 북마크 목록 가져오기 + try { + List bookmarks = bookmarkRepository.findByUserId(userId); + + return bookmarks.stream().map(bookmark -> { + Post post = bookmark.getPost(); + return new PostPreviewDto(post.getPostId(), post.getPostImage()); + }).collect(Collectors.toList()); + + } catch (Exception e) { + throw new CustomException(ErrorCode.DATABASE_ERROR); + } + } + + public UserDto updateUserId(int userId, String newUserId) { + // 사용자 확인 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 새로운 userId가 이미 존재하는지 확인 + if (userRepository.existsByUserId(newUserId)) { + throw new CustomException(ErrorCode.USER_ID_ALREADY_EXISTS); // 이미 존재하는 userId일 경우 예외 처리 + } + + // 사용자 userId 변경 + user.setUserId(newUserId); + userRepository.save(user); + + return UserDto.builder() + .userId(user.getUserId()) + .userName(user.getUserName()) + .profileImageUrl(user.getProfileImageUrl()) + .preferenceLevel(user.getPreferenceLevel()) + .build(); + } + + public void updatePreferenceLevel(int userId, Double preferenceLevel) { + // 사용자 확인 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 유효한 도수 값인지 검증 + if (preferenceLevel < 0 || preferenceLevel > 100) { + throw new CustomException(ErrorCode.INVALID_PREFERENCE_LEVEL); + } + + user.setPreferenceLevel(preferenceLevel); + userRepository.save(user); + } + + public String updateProfileImage(int userId, MultipartFile profileImage) { + // 사용자 확인 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 파일 저장 + String savedImageUrl = fileStorageService.storeFile(profileImage); + + // 사용자 프로필 이미지 URL 업데이트 + user.setProfileImageUrl(savedImageUrl); + userRepository.save(user); + + return savedImageUrl; + } +} + diff --git a/Backend/src/main/resources/application.properties b/Backend/src/main/resources/application.properties new file mode 100644 index 0000000..ce18e4c --- /dev/null +++ b/Backend/src/main/resources/application.properties @@ -0,0 +1,18 @@ +spring.application.name=web3_spring2 +spring.datasource.url=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect + +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +spring.sql.init.mode=always + +file.upload-dir=src/main/resources/static/images/uploads + +logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.com.example=DEBUG \ No newline at end of file diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju1.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju1.png new file mode 100644 index 0000000..6cb46af Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju1.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju10.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju10.png new file mode 100644 index 0000000..88866af Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju10.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju11.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju11.png new file mode 100644 index 0000000..1f4a9cf Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju11.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju12.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju12.png new file mode 100644 index 0000000..b30a84d Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju12.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju13.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju13.png new file mode 100644 index 0000000..7137b65 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju13.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju14.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju14.png new file mode 100644 index 0000000..5b89cb7 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju14.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju15.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju15.png new file mode 100644 index 0000000..17d5a2e Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju15.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju16.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju16.png new file mode 100644 index 0000000..bd8457e Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju16.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju2.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju2.png new file mode 100644 index 0000000..37e1fe8 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju2.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju3.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju3.png new file mode 100644 index 0000000..bcf28d1 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju3.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju4.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju4.png new file mode 100644 index 0000000..2508634 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju4.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju5.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju5.png new file mode 100644 index 0000000..6396038 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju5.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju6.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju6.png new file mode 100644 index 0000000..82a65a1 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju6.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju7.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju7.png new file mode 100644 index 0000000..8a6e54a Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju7.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju8.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju8.png new file mode 100644 index 0000000..da492da Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju8.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/cheongju9.png b/Backend/src/main/resources/static/images/cheongtakju/cheongju9.png new file mode 100644 index 0000000..31f732d Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/cheongju9.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju1.png b/Backend/src/main/resources/static/images/cheongtakju/takju1.png new file mode 100644 index 0000000..af07421 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju1.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju10.png b/Backend/src/main/resources/static/images/cheongtakju/takju10.png new file mode 100644 index 0000000..139fd28 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju10.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju11.png b/Backend/src/main/resources/static/images/cheongtakju/takju11.png new file mode 100644 index 0000000..98e2d09 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju11.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju12.png b/Backend/src/main/resources/static/images/cheongtakju/takju12.png new file mode 100644 index 0000000..1c4a267 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju12.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju13.png b/Backend/src/main/resources/static/images/cheongtakju/takju13.png new file mode 100644 index 0000000..289e2af Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju13.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju14.png b/Backend/src/main/resources/static/images/cheongtakju/takju14.png new file mode 100644 index 0000000..a1444af Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju14.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju15.png b/Backend/src/main/resources/static/images/cheongtakju/takju15.png new file mode 100644 index 0000000..148cca4 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju15.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju16.png b/Backend/src/main/resources/static/images/cheongtakju/takju16.png new file mode 100644 index 0000000..497ab10 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju16.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju17.png b/Backend/src/main/resources/static/images/cheongtakju/takju17.png new file mode 100644 index 0000000..0621182 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju17.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju18.png b/Backend/src/main/resources/static/images/cheongtakju/takju18.png new file mode 100644 index 0000000..a0cef80 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju18.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju19.png b/Backend/src/main/resources/static/images/cheongtakju/takju19.png new file mode 100644 index 0000000..63be8e1 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju19.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju2.png b/Backend/src/main/resources/static/images/cheongtakju/takju2.png new file mode 100644 index 0000000..2718312 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju2.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju20.png b/Backend/src/main/resources/static/images/cheongtakju/takju20.png new file mode 100644 index 0000000..e517387 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju20.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju21.png b/Backend/src/main/resources/static/images/cheongtakju/takju21.png new file mode 100644 index 0000000..d149339 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju21.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju22.png b/Backend/src/main/resources/static/images/cheongtakju/takju22.png new file mode 100644 index 0000000..f2ea4eb Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju22.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju23.png b/Backend/src/main/resources/static/images/cheongtakju/takju23.png new file mode 100644 index 0000000..13d3993 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju23.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju24.png b/Backend/src/main/resources/static/images/cheongtakju/takju24.png new file mode 100644 index 0000000..85d3ffa Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju24.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju25.png b/Backend/src/main/resources/static/images/cheongtakju/takju25.png new file mode 100644 index 0000000..25bf6ed Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju25.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju26.png b/Backend/src/main/resources/static/images/cheongtakju/takju26.png new file mode 100644 index 0000000..2ba43fa Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju26.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju27.png b/Backend/src/main/resources/static/images/cheongtakju/takju27.png new file mode 100644 index 0000000..83d4281 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju27.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju28.png b/Backend/src/main/resources/static/images/cheongtakju/takju28.png new file mode 100644 index 0000000..c3ef800 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju28.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju29.png b/Backend/src/main/resources/static/images/cheongtakju/takju29.png new file mode 100644 index 0000000..cf3eaa9 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju29.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju3.png b/Backend/src/main/resources/static/images/cheongtakju/takju3.png new file mode 100644 index 0000000..99d3089 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju3.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju30.png b/Backend/src/main/resources/static/images/cheongtakju/takju30.png new file mode 100644 index 0000000..5bcfd3f Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju30.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju31.png b/Backend/src/main/resources/static/images/cheongtakju/takju31.png new file mode 100644 index 0000000..4d45efa Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju31.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju32.png b/Backend/src/main/resources/static/images/cheongtakju/takju32.png new file mode 100644 index 0000000..a48578d Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju32.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju33.png b/Backend/src/main/resources/static/images/cheongtakju/takju33.png new file mode 100644 index 0000000..a402795 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju33.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju34.png b/Backend/src/main/resources/static/images/cheongtakju/takju34.png new file mode 100644 index 0000000..25f6a7d Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju34.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju35.png b/Backend/src/main/resources/static/images/cheongtakju/takju35.png new file mode 100644 index 0000000..71b5679 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju35.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju36.png b/Backend/src/main/resources/static/images/cheongtakju/takju36.png new file mode 100644 index 0000000..2507f00 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju36.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju37.png b/Backend/src/main/resources/static/images/cheongtakju/takju37.png new file mode 100644 index 0000000..9daa9f7 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju37.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju38.png b/Backend/src/main/resources/static/images/cheongtakju/takju38.png new file mode 100644 index 0000000..ecfe0a4 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju38.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju39.png b/Backend/src/main/resources/static/images/cheongtakju/takju39.png new file mode 100644 index 0000000..83d4440 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju39.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju4.png b/Backend/src/main/resources/static/images/cheongtakju/takju4.png new file mode 100644 index 0000000..9b41b35 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju4.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju40.png b/Backend/src/main/resources/static/images/cheongtakju/takju40.png new file mode 100644 index 0000000..2ede0e5 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju40.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju41.png b/Backend/src/main/resources/static/images/cheongtakju/takju41.png new file mode 100644 index 0000000..b6239f9 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju41.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju42.png b/Backend/src/main/resources/static/images/cheongtakju/takju42.png new file mode 100644 index 0000000..8190cce Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju42.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju43.png b/Backend/src/main/resources/static/images/cheongtakju/takju43.png new file mode 100644 index 0000000..e89cac2 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju43.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju44.png b/Backend/src/main/resources/static/images/cheongtakju/takju44.png new file mode 100644 index 0000000..9b360ab Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju44.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju45.png b/Backend/src/main/resources/static/images/cheongtakju/takju45.png new file mode 100644 index 0000000..361d585 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju45.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju46.png b/Backend/src/main/resources/static/images/cheongtakju/takju46.png new file mode 100644 index 0000000..abde38f Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju46.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju47.png b/Backend/src/main/resources/static/images/cheongtakju/takju47.png new file mode 100644 index 0000000..dab0930 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju47.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju48.png b/Backend/src/main/resources/static/images/cheongtakju/takju48.png new file mode 100644 index 0000000..23ef728 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju48.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju49.png b/Backend/src/main/resources/static/images/cheongtakju/takju49.png new file mode 100644 index 0000000..def0028 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju49.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju5.png b/Backend/src/main/resources/static/images/cheongtakju/takju5.png new file mode 100644 index 0000000..ffa373f Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju5.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju50.png b/Backend/src/main/resources/static/images/cheongtakju/takju50.png new file mode 100644 index 0000000..12709a2 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju50.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju6.png b/Backend/src/main/resources/static/images/cheongtakju/takju6.png new file mode 100644 index 0000000..83c3229 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju6.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju7.png b/Backend/src/main/resources/static/images/cheongtakju/takju7.png new file mode 100644 index 0000000..d9fe6d2 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju7.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju8.png b/Backend/src/main/resources/static/images/cheongtakju/takju8.png new file mode 100644 index 0000000..26e0095 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju8.png differ diff --git a/Backend/src/main/resources/static/images/cheongtakju/takju9.png b/Backend/src/main/resources/static/images/cheongtakju/takju9.png new file mode 100644 index 0000000..81f1be6 Binary files /dev/null and b/Backend/src/main/resources/static/images/cheongtakju/takju9.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/1168SweetWine.png b/Backend/src/main/resources/static/images/fruitWine/1168SweetWine.png new file mode 100644 index 0000000..74078cd Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/1168SweetWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/559SweetRedWine.png b/Backend/src/main/resources/static/images/fruitWine/559SweetRedWine.png new file mode 100644 index 0000000..b702c73 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/559SweetRedWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/Aropure.png b/Backend/src/main/resources/static/images/fruitWine/Aropure.png new file mode 100644 index 0000000..6f66ade Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/Aropure.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/BancoreDeogam.png b/Backend/src/main/resources/static/images/fruitWine/BancoreDeogam.png new file mode 100644 index 0000000..959e1a1 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/BancoreDeogam.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/BiwonPure.png b/Backend/src/main/resources/static/images/fruitWine/BiwonPure.png new file mode 100644 index 0000000..0495cb4 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/BiwonPure.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/CabernetSauvignon.png b/Backend/src/main/resources/static/images/fruitWine/CabernetSauvignon.png new file mode 100644 index 0000000..1b924c5 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/CabernetSauvignon.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/CheekyPeach.png b/Backend/src/main/resources/static/images/fruitWine/CheekyPeach.png new file mode 100644 index 0000000..5f036db Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/CheekyPeach.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/Choryun.png b/Backend/src/main/resources/static/images/fruitWine/Choryun.png new file mode 100644 index 0000000..446f0cf Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/Choryun.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/ClonerAndSweetWine.png b/Backend/src/main/resources/static/images/fruitWine/ClonerAndSweetWine.png new file mode 100644 index 0000000..1eb013d Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/ClonerAndSweetWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/GimcheonUniversityPlumWine.png b/Backend/src/main/resources/static/images/fruitWine/GimcheonUniversityPlumWine.png new file mode 100644 index 0000000..7776247 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/GimcheonUniversityPlumWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/GodoriRoseWine.png b/Backend/src/main/resources/static/images/fruitWine/GodoriRoseWine.png new file mode 100644 index 0000000..d55cdb1 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/GodoriRoseWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/GradcoteauFreshWater.png b/Backend/src/main/resources/static/images/fruitWine/GradcoteauFreshWater.png new file mode 100644 index 0000000..f8ae4ed Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/GradcoteauFreshWater.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/HamianDry.png b/Backend/src/main/resources/static/images/fruitWine/HamianDry.png new file mode 100644 index 0000000..aac317e Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/HamianDry.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/HamianOakWine.png b/Backend/src/main/resources/static/images/fruitWine/HamianOakWine.png new file mode 100644 index 0000000..6690c3b Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/HamianOakWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/HamiangSweet.png b/Backend/src/main/resources/static/images/fruitWine/HamiangSweet.png new file mode 100644 index 0000000..721ad83 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/HamiangSweet.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/Hwamong.png b/Backend/src/main/resources/static/images/fruitWine/Hwamong.png new file mode 100644 index 0000000..01cf9c9 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/Hwamong.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/KratteWhiteSweet.png b/Backend/src/main/resources/static/images/fruitWine/KratteWhiteSweet.png new file mode 100644 index 0000000..dbf4d71 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/KratteWhiteSweet.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/MediumDry.png b/Backend/src/main/resources/static/images/fruitWine/MediumDry.png new file mode 100644 index 0000000..b4786bb Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/MediumDry.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/MellonSeoli.png b/Backend/src/main/resources/static/images/fruitWine/MellonSeoli.png new file mode 100644 index 0000000..9d15ef2 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/MellonSeoli.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/MeruWine.png b/Backend/src/main/resources/static/images/fruitWine/MeruWine.png new file mode 100644 index 0000000..22a8992 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/MeruWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/MeruWineDeepDry.png b/Backend/src/main/resources/static/images/fruitWine/MeruWineDeepDry.png new file mode 100644 index 0000000..d31a8d9 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/MeruWineDeepDry.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/MiratoRoseWine.png b/Backend/src/main/resources/static/images/fruitWine/MiratoRoseWine.png new file mode 100644 index 0000000..abd7b21 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/MiratoRoseWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/Moon1614SweetWine.png b/Backend/src/main/resources/static/images/fruitWine/Moon1614SweetWine.png new file mode 100644 index 0000000..0d7162d Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/Moon1614SweetWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/MuscatBaileyA.png b/Backend/src/main/resources/static/images/fruitWine/MuscatBaileyA.png new file mode 100644 index 0000000..cfceefc Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/MuscatBaileyA.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/NervneSparklingAppleWine.png b/Backend/src/main/resources/static/images/fruitWine/NervneSparklingAppleWine.png new file mode 100644 index 0000000..df0d084 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/NervneSparklingAppleWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/NeubnaeRoseSparkling.png b/Backend/src/main/resources/static/images/fruitWine/NeubnaeRoseSparkling.png new file mode 100644 index 0000000..bb54d30 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/NeubnaeRoseSparkling.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/OakWine.png b/Backend/src/main/resources/static/images/fruitWine/OakWine.png new file mode 100644 index 0000000..33695f1 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/OakWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/OlosiPeache.png b/Backend/src/main/resources/static/images/fruitWine/OlosiPeache.png new file mode 100644 index 0000000..13312e0 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/OlosiPeache.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/PSWhiteSparkling.png b/Backend/src/main/resources/static/images/fruitWine/PSWhiteSparkling.png new file mode 100644 index 0000000..d5473b8 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/PSWhiteSparkling.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/PoemRoseWine.png b/Backend/src/main/resources/static/images/fruitWine/PoemRoseWine.png new file mode 100644 index 0000000..a291a88 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/PoemRoseWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/PoemWhiteWine.png b/Backend/src/main/resources/static/images/fruitWine/PoemWhiteWine.png new file mode 100644 index 0000000..61aed76 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/PoemWhiteWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/RoseCiderOmihanjan.png b/Backend/src/main/resources/static/images/fruitWine/RoseCiderOmihanjan.png new file mode 100644 index 0000000..5fcfb05 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/RoseCiderOmihanjan.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/RoseSweetWine.png b/Backend/src/main/resources/static/images/fruitWine/RoseSweetWine.png new file mode 100644 index 0000000..9f468bb Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/RoseSweetWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/SaintHouseApricotWine.png b/Backend/src/main/resources/static/images/fruitWine/SaintHouseApricotWine.png new file mode 100644 index 0000000..18cb433 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/SaintHouseApricotWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/ShineMuscatWhiteWine.png b/Backend/src/main/resources/static/images/fruitWine/ShineMuscatWhiteWine.png new file mode 100644 index 0000000..4054825 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/ShineMuscatWhiteWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/SobakSweetRedWine.png b/Backend/src/main/resources/static/images/fruitWine/SobakSweetRedWine.png new file mode 100644 index 0000000..6455e77 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/SobakSweetRedWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/SparklingWhite.png b/Backend/src/main/resources/static/images/fruitWine/SparklingWhite.png new file mode 100644 index 0000000..de39688 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/SparklingWhite.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/SweetSweet.png b/Backend/src/main/resources/static/images/fruitWine/SweetSweet.png new file mode 100644 index 0000000..3c2c23d Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/SweetSweet.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/SyatomisoIceWine.png b/Backend/src/main/resources/static/images/fruitWine/SyatomisoIceWine.png new file mode 100644 index 0000000..3eac1fc Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/SyatomisoIceWine.png differ diff --git a/Backend/src/main/resources/static/images/fruitWine/WeddingPlumWine.png b/Backend/src/main/resources/static/images/fruitWine/WeddingPlumWine.png new file mode 100644 index 0000000..be51942 Binary files /dev/null and b/Backend/src/main/resources/static/images/fruitWine/WeddingPlumWine.png differ diff --git a/Backend/src/main/resources/static/images/uploads/1731598889462_profiletest.png b/Backend/src/main/resources/static/images/uploads/1731598889462_profiletest.png new file mode 100644 index 0000000..533377e Binary files /dev/null and b/Backend/src/main/resources/static/images/uploads/1731598889462_profiletest.png differ diff --git a/Backend/src/test/java/com/web3/Backend/Web3Spring2ApplicationTests.java b/Backend/src/test/java/com/web3/Backend/Web3Spring2ApplicationTests.java new file mode 100644 index 0000000..5455839 --- /dev/null +++ b/Backend/src/test/java/com/web3/Backend/Web3Spring2ApplicationTests.java @@ -0,0 +1,13 @@ +package com.web3.Backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Web3Spring2ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Backend/system.properties b/Backend/system.properties new file mode 100644 index 0000000..0dc726c --- /dev/null +++ b/Backend/system.properties @@ -0,0 +1 @@ +java.runtime.version=17 \ No newline at end of file diff --git a/client/.env b/client/.env new file mode 100644 index 0000000..0823ba4 --- /dev/null +++ b/client/.env @@ -0,0 +1 @@ +REACT_APP_API_ROUTE=https://foreign-papagena-wap2024-2-web3-0d04a01a.koyeb.app \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..f768e33 --- /dev/null +++ b/client/README.md @@ -0,0 +1,8 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..be6887e --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,40 @@ +import js from "@eslint/js"; +import globals from "globals"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; + +export default [ + { ignores: ["dist"] }, + { + files: ["**/*.{js,jsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: "latest", + ecmaFeatures: { jsx: true }, + sourceType: "module", + }, + }, + settings: { react: { version: "18.3" } }, + plugins: { + react, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + ...reactHooks.configs.recommended.rules, + "react/jsx-no-target-blank": "off", + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "no-unused-vars": "off", + "react/prop-types": "off", + }, + }, +]; diff --git a/client/netlify.toml b/client/netlify.toml new file mode 100644 index 0000000..ff0adf0 --- /dev/null +++ b/client/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "CI= npm run build" + publish = "build" \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 4e2f30e..7b051c2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,9 +11,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", + "framer-motion": "^11.11.10", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-lazyload": "^3.2.1", + "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "styled-components": "^6.1.13", "web-vitals": "^2.1.4" } }, @@ -2436,6 +2441,27 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3540,6 +3566,15 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4817,6 +4852,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -5811,6 +5852,31 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -6410,6 +6476,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -6883,6 +6958,15 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -7032,6 +7116,17 @@ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "license": "MIT" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -9437,6 +9532,31 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.11.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.10.tgz", + "integrity": "sha512-061Bt1jL/vIm+diYIiA4dP/Yld7vD47ROextS7ESBW5hr4wQFhxB5D5T5zAc3c/5me3cOa+iO5LqhA38WDln/A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -15829,6 +15949,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -16206,6 +16332,16 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, + "node_modules/react-lazyload": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/react-lazyload/-/react-lazyload-3.2.1.tgz", + "integrity": "sha512-oDLlLOI/rRLY0fUh/HYFCy4CqCe7zdJXv6oTl2pC30tN3ezWxvwcdHYfD/ZkrGOMOOT5pO7hNLSvg7WsmAij1w==", + "license": "MIT", + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -16215,6 +16351,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -17159,6 +17327,12 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -17771,6 +17945,68 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -17787,6 +18023,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/client/package.json b/client/package.json index ce5a392..a8cc444 100644 --- a/client/package.json +++ b/client/package.json @@ -6,9 +6,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", + "framer-motion": "^11.11.10", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-lazyload": "^3.2.1", + "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "styled-components": "^6.1.13", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/client/public/_redirects b/client/public/_redirects new file mode 100644 index 0000000..b21f6cb --- /dev/null +++ b/client/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/client/public/data/drinks.json b/client/public/data/drinks.json new file mode 100644 index 0000000..7a73493 --- /dev/null +++ b/client/public/data/drinks.json @@ -0,0 +1,705 @@ +[ +{ + "drinkName": "치키피치", + "preferenceLevel":4.6, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/CheekyPeach.jpg" +},{ + "drinkName": "P.S 화이트 스파클링", + "preferenceLevel":5, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/PS_WhiteSparkling.jpg" +},{ + "drinkName": "멜론서리", + "preferenceLevel":5, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/MellonSeoli.jpg" +},{ + "drinkName": "오롯이 복숭아", + "preferenceLevel":9, + "area": "세종특별자치시", + "type": "과실주", + "postImage": "/images/fruitWine/OlosiPeache.jpg" +},{ + "drinkName": "너브내스파클링사과와인", + "preferenceLevel":8.5, + "area": "강원도", + "type": "과실주", + "postImage": "/images/fruitWine/NervneSparklingAppleWine.jpg" +},{ + "drinkName": "로제사이더 오미한잔", + "preferenceLevel":4.5, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/RoseCiderOmihanjan.jpg" +},{ + "drinkName": "세인트하우스 살구와인", + "preferenceLevel":12, + "area": "충청남도 ", + "type": "과실주", + "postImage": "/images/fruitWine/SaintHouseApricotWine.jpg" +},{ + "drinkName": "머루와인", + "preferenceLevel":12, + "area": "전라북도", + "type": "과실주", + "postImage": "/images/fruitWine/MeruWine.jpg" +},{ + "drinkName": "그랑꼬또 청수", + "preferenceLevel":12, + "area": "경기도", + "type": "과실주", + "postImage": "/images/fruitWine/GradcoteauFreshWater.jpg" +},{ + "drinkName": "베베마루 설레임 로제 스위트와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/RoséSweetWine.jpg" +},{ + "drinkName": "산막와이너리, 화몽", + "preferenceLevel":13, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/Hwamong.jpg" +},{ + "drinkName": "산막와이너리, 비원퓨어", + "preferenceLevel":13, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/BiwonPure.jpg" +},{ + "drinkName": "크라테 화이트스위트", + "preferenceLevel":11.5, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/KratteWhiteSweet.jpg" +},{ + "drinkName": "김천대학교 자두사랑 자두와인", + "preferenceLevel":12, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/GimcheonUniversityPlumWine.jpg" +},{ + "drinkName": "미르아토 로제와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/MiratoRoseWine.jpg" +},{ + "drinkName": "산머루와인 달콤한 스위트", + "preferenceLevel":10.5, + "area": "경상남도", + "type": "과실주", + "postImage": "/images/fruitWine/SweetSweet.jpg" +},{ + "drinkName": "2016년산 하미앙 드라이", + "preferenceLevel":12, + "area": "경상남도", + "type": "과실주", + "postImage": "/images/fruitWine/HamianDry.jpg" +},{ + "drinkName": "고도리 로제와인", + "preferenceLevel":12, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/GodoriRoseWine.jpg" +},{ + "drinkName": "소계리 595 스위트 레드와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/559SweetRedWine.jpg" +},{ + "drinkName": "끌로너와 스위트와인", + "preferenceLevel":12, + "area": "강원도", + "type": "과실주", + "postImage": "/images/fruitWine/ClonerAndSweetWine.jpg" +},{ + "drinkName": "고도리 샤인머스캣 화이트와인", + "preferenceLevel":10.5, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/ShineMuscatWhiteWine.jpg" +},{ + "drinkName": "포엠 화이트와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/PoemWhiteWine.jpg" +},{ + "drinkName": "산막와이너리, 초련", + "preferenceLevel":20, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/Choryun.jpg" +},{ + "drinkName": "코이버펑크, 머스캣 베일리 에이 2017", + "preferenceLevel":12.5, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/MuscatBaileyA.jpg" +},{ + "drinkName": "산막와이너리, 아로퓨어", + "preferenceLevel":13, + "area": "충청북도 ", + "type": "과실주", + "postImage": "/images/fruitWine/Aropure.jpg" +},{ + "drinkName": "뱅꼬레 더감", + "preferenceLevel":12, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/BancoreDeogam.jpg" +},{ + "drinkName": "샤토미소 아이스와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/SyatomisoIceWine.jpg" +},{ + "drinkName": "산머루와인 깊은맛 드라이", + "preferenceLevel":12, + "area": "경라남도", + "type": "과실주", + "postImage": "/images/fruitWine/MeruWineDeepDry.jpg" +},{ + "drinkName": "너브내 로제 스파클링", + "preferenceLevel":12, + "area": "강원도", + "type": "과실주", + "postImage": "/images/fruitWine/NeubnaeRoséSparkling.jpg" +},{ + "drinkName": "달 1614 스위트와인", + "preferenceLevel":12, + "area": "전라북도", + "type": "과실주", + "postImage": "/images/fruitWine/Moon1614SweetWine.jpg" +},{ + "drinkName": "샤토미소 웨딩 자두와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/WeddingPlumWine.jpg" +},{ + "drinkName": "코이버펑크, 카베르네 소비뇽 2019", + "preferenceLevel":12.5, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/CabernetSauvignon.jpg" +},{ + "drinkName": "포엠 로제와인", + "preferenceLevel":12, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/PoemRoséWine.jpg" +},{ + "drinkName": "2016년산 하미앙 스위트", + "preferenceLevel":10.5, + "area": "경라남도", + "type": "과실주", + "postImage": "/images/fruitWine/HamiangSweet.jpg" +},{ + "drinkName": "베리와인1168 스위트와인", + "preferenceLevel":13, + "area": "충청북도", + "type": "과실주", + "postImage": "/images/fruitWine/1168SweetWine.jpg" +},{ + "drinkName": "오크와인", + "preferenceLevel":12, + "area": "경라남도", + "type": "과실주", + "postImage": "/images/fruitWine/OakWine.jpg" +},{ + "drinkName": "너브내 스파클링 화이트", + "preferenceLevel":12, + "area": "강원도", + "type": "과실주", + "postImage": "/images/fruitWine/SparklingWhite.jpg" +},{ + "drinkName": "소백산(샤토소백) 스위트 레드와인", + "preferenceLevel":12, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/SobakSweetRedWine.jpg" +},{ + "drinkName": "크라테 미디엄드라이", + "preferenceLevel":11.5, + "area": "경상북도", + "type": "과실주", + "postImage": "/images/fruitWine/MediumDry.jpg" +},{ + "drinkName": "2018년산 하미앙 오크와인", + "preferenceLevel":12, + "area": "경라남도", + "type": "과실주", + "postImage": "/images/fruitWine/HamianOakWine.jpg" +}, + + {"drinkName":"소풍", + "preferenceLevel":5.5, + "area":"경상남도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju1.jpg" + }, + + {"drinkName":"가와지탁주", + "preferenceLevel":7.5, + "area":"경기도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju2.jpg" + }, + + {"drinkName":"연희유자", + "preferenceLevel":10, + "area":"서울특별시", + "type":"탁주", + "postImage":"/images/cheongtakju/takju3.jpg" + }, + + {"drinkName":"팔팔막걸리", + "preferenceLevel":6, + "area":"경기도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju4.jpg" + }, + + {"drinkName":"설레 7도", + "preferenceLevel":7, + "area":"경상남도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju5.jpg" + }, + + {"drinkName":"보은주", + "preferenceLevel":10, + "area":"충청북도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju6.jpg" + }, + + {"drinkName":"바텐더의막걸리", + "preferenceLevel":14, + "area":"서울특별시", + "type":"탁주", + "postImage":"/images/cheongtakju/takju7.jpg" + }, + + {"drinkName":"김유정역", + "preferenceLevel":6, + "area":"강원도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju8.jpg" + }, + + {"drinkName":"님 그리다", + "preferenceLevel":6, + "area":"경상남도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju9.jpg" + }, + + {"drinkName":"일엽편주", + "preferenceLevel":12, + "area":"경상북도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju10.jpg" + }, + + {"drinkName":"입장탁주", + "preferenceLevel":7, + "area":"충청남도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju11.jpg" + }, + + {"drinkName":"메들리손막걸리", + "preferenceLevel":8, + "area":"경기도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju12.jpg" + }, + + {"drinkName":"자작막걸리", + "preferenceLevel":12, + "area":"강원도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju13.jpg" + }, + + {"drinkName":"냥이탁주 화이트", + "preferenceLevel":10.5, + "area":"경기도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju14.jpg" + }, + + {"drinkName":"대대포9", + "preferenceLevel":9, + "area":"전라남도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju15.jpg" + }, + + {"drinkName":"아임프리6.0", + "preferenceLevel":6, + "area":"경기도", + "type":"탁주", + "postImage":"/images/cheongtakju/takju16.jpg" + }, + + {"drinkName":"냥이탁주 9도", + "preferenceLevel":9, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju17.jpg" + }, + + {"drinkName":"골목막걸리 프리미엄 12", + "preferenceLevel":12, + "area":"충청남도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju18.jpg" + }, + + {"drinkName":"과천미주", + "preferenceLevel":9, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju19.jpg" + }, + + {"drinkName":"복순도가 슈퍼드라이", + "preferenceLevel":6.5, + "area":"울산광역시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju20.jpg" + }, + + {"drinkName":"옹근달 본막걸리", + "preferenceLevel":15, + "area":"인천광역시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju21.jpg" + }, + + {"drinkName":"산정호수 동정춘막걸리", + "preferenceLevel":6, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju22.jpg" + }, + + {"drinkName":"단홍", + "preferenceLevel":13.5, + "area":"서울특별시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju23.jpg" + }, + + {"drinkName":"오산막걸리", + "preferenceLevel":6, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju24.jpg" + }, + + {"drinkName":"일곱쌀", + "preferenceLevel":7, + "area":"서울특별시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju25.jpg" + }, + + {"drinkName":"탁112클래식", + "preferenceLevel":12, + "area":"인천광역시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju26.jpg" + }, + + {"drinkName":"생 옥수수 동동주", + "preferenceLevel":6, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju27.jpg" + }, + + {"drinkName":"담향 대대포 블루", + "preferenceLevel":6, + "area":"전라남도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju28.jpg" + }, + + {"drinkName":"도갓집 막걸리", + "preferenceLevel":6, + "area":"전라남도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju29.jpg" + }, + + {"drinkName":"한영석 하향주", + "preferenceLevel":13.8, + "area":"전라북도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju30.jpg" + }, + + {"drinkName":"이화주(술샘)", + "preferenceLevel":8, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju31.jpg" + }, + + {"drinkName":"관악산생막걸리", + "preferenceLevel":6, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju32.jpg" + }, + + {"drinkName":"하얀까마귀", + "preferenceLevel":8, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju33.jpg" + }, + + {"drinkName":"정감생막걸리", + "preferenceLevel":6, + "area":"경상남도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju34.jpg" + }, + + {"drinkName":"눈내린여름밤", + "preferenceLevel":12, + "area":"서울특별시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju35.jpg" + }, + + {"drinkName":"한강의설레임", + "preferenceLevel":11, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju36.jpg" + }, + + {"drinkName":"시향가 미니캔막걸리", + "preferenceLevel":8, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju37.jpg" + }, + + {"drinkName":"강냉이 막걸리", + "preferenceLevel":6, + "area":"충청북도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju38.jpg" + }, + + {"drinkName":"말이야 막걸리야", + "preferenceLevel":6, + "area":"전라남도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju39.jpg" + }, + + {"drinkName":"연꽃담은술", + "preferenceLevel":8, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju40.jpg" + }, + + {"drinkName":"냥이탁주 fresh", + "preferenceLevel":5, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju41.jpg" + }, + + {"drinkName":"주홍춘", + "preferenceLevel":10, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju42.jpg" + }, + + {"drinkName":"양조학당 뜰", + "preferenceLevel":12, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju43.jpg" + }, + + {"drinkName":"금풍양조", + "preferenceLevel":6.9, + "area":"인천광역시", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju44.jpg" + }, + + {"drinkName":"영일만친구 막걸리", + "preferenceLevel":6, + "area":"경상북도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju45.jpg" + }, + + {"drinkName":"선희주", + "preferenceLevel":12, + "area":"충청북도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju46.jpg" + }, + + {"drinkName":"연오랑탁주", + "preferenceLevel":12, + "area":"경상북도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju47.jpg" + }, + + {"drinkName":"청혼 화이트", + "preferenceLevel":13, + "area":"경기도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju48.jpg" + }, + + {"drinkName":"오희 스파클링 막걸리", + "preferenceLevel":8.5, + "area":"경상북도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju49.jpg" + }, + + {"drinkName":"곤드레생막걸리", + "preferenceLevel":6, + "area":"강원도", + "type":"탁주", + "postIamge":"/images/cheongtakju/takju50.jpg" + }, + + {"drinkName":"세종대왕어주 약주", + "preferenceLevel":15, + "area":"충청북도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju1.jpg" + }, + + {"drinkName":"주교주", + "preferenceLevel":16, + "area":"경기도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju2.jpg" + }, + + {"drinkName":"일엽편주", + "preferenceLevel":15, + "area":"경상북도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju3.jpg" + }, + + {"drinkName":"모월 연", + "preferenceLevel":13, + "area":"강원도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju4.jpg" + }, + + {"drinkName":"메들리아카시아", + "preferenceLevel":7, + "area":"경기도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju5.jpg" + }, + + {"drinkName":"경산대추약주 추", + "preferenceLevel":16, + "area":"경상북도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju6.jpg" + }, + + {"drinkName":"보리수헤는밤", + "preferenceLevel":8, + "area":"충청남도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju7.jpg" + }, + + {"drinkName":"지란지교 약주", + "preferenceLevel":17, + "area":"전라북도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju8.jpg" + }, + + {"drinkName":"모월 청", + "preferenceLevel":16, + "area":"강원도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju9.jpg" + }, + + {"drinkName":"담골드", + "preferenceLevel":12, + "area":"경기도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju10.jpg" + }, + + {"drinkName":"청혼 블루", + "preferenceLevel":15, + "area":"경기도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju11.jpg" + }, + + {"drinkName":"복단지", + "preferenceLevel":14, + "area":"경기도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju12.jpg" + }, + + {"drinkName":"서설", + "preferenceLevel":13, + "area":"경기도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju13.jpg" + }, + + {"drinkName":"호산춘", + "preferenceLevel":18, + "area":"경상북도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju14.jpg" + }, + + {"drinkName":"일지춘", + "preferenceLevel":15, + "area":"강원도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju15.jpg" + }, + + {"drinkName":"과하주 온", + "preferenceLevel":18.5, + "area":"강원도", + "type":"청주", + "postIamge":"/images/cheongtakju/cheongju16.jpg" + } +] \ No newline at end of file diff --git a/client/public/images/Holjjak-logo.png b/client/public/images/Holjjak-logo.png new file mode 100644 index 0000000..17a8372 Binary files /dev/null and b/client/public/images/Holjjak-logo.png differ diff --git a/client/public/images/logo-white.png b/client/public/images/logo-white.png new file mode 100644 index 0000000..4995e39 Binary files /dev/null and b/client/public/images/logo-white.png differ diff --git a/client/public/images/mainpage/banner-img-figma.png b/client/public/images/mainpage/banner-img-figma.png new file mode 100644 index 0000000..c1f568c Binary files /dev/null and b/client/public/images/mainpage/banner-img-figma.png differ diff --git a/client/public/images/mainpage/category-img1.png b/client/public/images/mainpage/category-img1.png new file mode 100644 index 0000000..ac1b5a3 Binary files /dev/null and b/client/public/images/mainpage/category-img1.png differ diff --git a/client/public/images/mainpage/category-img2.png b/client/public/images/mainpage/category-img2.png new file mode 100644 index 0000000..bd8e9d7 Binary files /dev/null and b/client/public/images/mainpage/category-img2.png differ diff --git a/client/public/images/mainpage/category-img3.png b/client/public/images/mainpage/category-img3.png new file mode 100644 index 0000000..98c61d2 Binary files /dev/null and b/client/public/images/mainpage/category-img3.png differ diff --git a/client/public/images/mainpage/mypage-btn-img.png b/client/public/images/mainpage/mypage-btn-img.png new file mode 100644 index 0000000..152b03a Binary files /dev/null and b/client/public/images/mainpage/mypage-btn-img.png differ diff --git a/client/public/images/mainpage/sub-category-img1.png b/client/public/images/mainpage/sub-category-img1.png new file mode 100644 index 0000000..b87617e Binary files /dev/null and b/client/public/images/mainpage/sub-category-img1.png differ diff --git a/client/public/images/mainpage/sub-category-img2.png b/client/public/images/mainpage/sub-category-img2.png new file mode 100644 index 0000000..1e2bd72 Binary files /dev/null and b/client/public/images/mainpage/sub-category-img2.png differ diff --git a/client/public/images/mainpage/sub-category-img3.png b/client/public/images/mainpage/sub-category-img3.png new file mode 100644 index 0000000..00f3431 Binary files /dev/null and b/client/public/images/mainpage/sub-category-img3.png differ diff --git a/client/public/index.html b/client/public/index.html index aa069f2..b06c78a 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -9,35 +9,24 @@ name="description" content="Web site created using create-react-app" /> - - - - React App + + + + + + Holjjak -
- diff --git a/client/public/manifest.json b/client/public/manifest.json index 080d6c7..ead4e20 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -1,23 +1,7 @@ { "short_name": "React App", "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], + "icons": [], "start_url": ".", "display": "standalone", "theme_color": "#000000", diff --git a/client/src/App.css b/client/src/App.css index e69de29..2493819 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -0,0 +1,10 @@ +* { + margin: 0; + padding: 0; +} + +body { + margin: 0; + padding: 0; + font-family: "Pretendard", sans-serif; +} diff --git a/client/src/App.js b/client/src/App.js index 9d18e93..6320009 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,7 +1,27 @@ import "./App.css"; +import MainPage from "./pages/MainPage"; +import CheongtakjuPage from "./pages/CheongtakjuPage"; +import FruitWinePage from "./pages/FruitWinePage"; +import SignInPage from "./pages/SignInPage"; +import Mypage from "./pages/mypage"; +import SignUpPage from "./pages/SignUpPage"; +import { BrowserRouter, Router, Route, Routes, Link } from "react-router-dom"; function App() { - return
; + return ( + <> + + + } /> + } />{" "} + } />{" "} + } /> + } /> + } /> + + + + ); } export default App; diff --git a/client/src/api/apiClient.js b/client/src/api/apiClient.js new file mode 100644 index 0000000..1dedecc --- /dev/null +++ b/client/src/api/apiClient.js @@ -0,0 +1,8 @@ +import axios from "axios"; + +// Axios 인스턴스 생성 +const apiClient = axios.create({ + baseURL: process.env.REACT_APP_API_ROUTE, // .env에서 설정한 baseURL 사용 +}); + +export default apiClient; diff --git a/client/src/api/cheongTakjuListApi.js b/client/src/api/cheongTakjuListApi.js new file mode 100644 index 0000000..cb7c88e --- /dev/null +++ b/client/src/api/cheongTakjuListApi.js @@ -0,0 +1,14 @@ +import apiClient from "./apiClient"; + +const cheongTakjuListApi = async (page) => { + try { + const response = await apiClient.get(`api/post/cheongtakju/${page}`); // baseURL 포함된 경로로 요청 + // console.log("서버 응답 메시지:", response.data.message); // 서버 응답 메시지 출력 + return response.data.data; // 필요한 데이터 반환 + } catch (error) { + console.log("API 호출 중 에러 발생:", error.message); + throw error; + } +}; + +export default cheongTakjuListApi; diff --git a/client/src/api/fruitWineListApi.js b/client/src/api/fruitWineListApi.js new file mode 100644 index 0000000..985fbdb --- /dev/null +++ b/client/src/api/fruitWineListApi.js @@ -0,0 +1,14 @@ +import apiClient from "./apiClient"; + +const fruitWineListApi = async (page) => { + try { + const response = await apiClient.get(`api/post/fruitWine/${page}`); // baseURL 포함된 경로로 요청 + // console.log("서버 응답 메시지:", response.data.message); // 서버 응답 메시지 출력 + return response.data.data; // 필요한 데이터 반환 + } catch (error) { + console.log("API 호출 중 에러 발생:", error.message); + throw error; + } +}; + +export default fruitWineListApi; diff --git a/client/src/api/logoutApi.js b/client/src/api/logoutApi.js new file mode 100644 index 0000000..4bb9c2e --- /dev/null +++ b/client/src/api/logoutApi.js @@ -0,0 +1,23 @@ +import apiClient from "./apiClient"; + +const logoutApi = async (refreshToken) => { + try { + const response = await apiClient.post( + "/auth/logout", + {}, // POST 요청의 body + { + headers: { + Authorization: `Bearer ${refreshToken}`, // 리프레시 토큰 전달 + }, + } + ); + + console.log("서버 응답 메시지:", response.data.message); + return response.data; // 성공 시 응답 데이터 반환 + } catch (error) { + console.log("로그아웃 API 에러:", error.name, "/", error.message); + throw error; + } +}; + +export default logoutApi; diff --git a/client/src/components/alcoholList/AlcoholList.css b/client/src/components/alcoholList/AlcoholList.css new file mode 100644 index 0000000..d980110 --- /dev/null +++ b/client/src/components/alcoholList/AlcoholList.css @@ -0,0 +1,89 @@ +.AlcoholList { + display: flex; + flex-direction: column; + justify-content: center; + margin: 5%; + align-items: center; +} + +.alcohol-container { + display: grid; + grid-template-columns: repeat(5, 1fr); /* 5열 */ + gap: 3%; /* 아이템 간격 */ + width: 75%; +} + +.alcohol-item-wrap { + display: flex; + flex-direction: column; /* 이미지와 텍스트 수직 정렬 */ + align-items: center; + text-align: center; + padding: 10px; + text-decoration: none; +} + +.alcohol-image { + width: 100%; + height: auto; + aspect-ratio: 5 / 6; + object-fit: contain; + border-radius: 8px; + background-color: rgb(239, 239, 239); +} + +.alcohol-name { + margin-top: 8px; + font-size: 13px; + font-weight: bold; + color: rgb(78, 75, 69); + + min-width: 100px; + max-width: 100px; /* 부모 요소 너비 제한 */ +} + +.alcohol-image:hover, +.alcohol-name:hover { + cursor: pointer; +} + +.link-img-tag, +.link-name-tag { + text-decoration: none; + color: inherit; + display: inline-block; +} + +/* 페이지네이션 CSS */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + margin-top: 3%; +} + +.pagination button { + margin: 0 5px; + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + border: none; + background-color: rgba(255, 255, 255, 0); + border-radius: 4px; + transition: background-color 0.3s; +} + +.pagination button:hover { + background-color: rgba(75, 72, 68, 0.445); + color: white; +} + +.pagination button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.pagination .active-page { + font-weight: bold; + background-color: rgba(255, 255, 255, 0); + color: rgb(0, 0, 0); +} diff --git a/client/src/components/alcoholList/AlcoholList.js b/client/src/components/alcoholList/AlcoholList.js new file mode 100644 index 0000000..7a57a4b --- /dev/null +++ b/client/src/components/alcoholList/AlcoholList.js @@ -0,0 +1,84 @@ +import "./AlcoholList.css"; +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { Link } from "react-router-dom"; + +// AlcoholList.js +const AlcoholList = ({ fetchApi }) => { + const [alcoholList, setAlcoholList] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + // 페이지 데이터를 불러오는 함수 + const fetchData = useCallback( + async (pageNum) => { + try { + const data = await fetchApi(pageNum); + setAlcoholList(data.content); + setTotalPages(data.totalPages); + } catch (error) { + console.log("데이터 로딩 중 오류:", error.message); + } + }, + [fetchApi] + ); + + // 페이지가 변경될 때만 데이터 호출 + useEffect(() => { + fetchData(page); + }, [page, fetchData]); + + const renderPageNumbers = () => { + const pageNumbers = []; + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push( + + ); + } + return pageNumbers; + }; + + return ( +
+
+ {alcoholList.map((item) => ( +
+ + {item.drinkName} + + +
{item.drinkName}
+ +
+ ))} +
+ +
+ + {renderPageNumbers()} + +
+
+ ); +}; + +export default AlcoholList; diff --git a/client/src/components/common/Footer.css b/client/src/components/common/Footer.css new file mode 100644 index 0000000..ea05cbd --- /dev/null +++ b/client/src/components/common/Footer.css @@ -0,0 +1,52 @@ +.Footer { + display: flex; + justify-content: space-between; + background-color: rgba(128, 128, 128, 0.477); + padding: 2% 10%; + /* background-color: transparent; */ +} + +.footer-wrap { + text-align: left; + flex-grow: 2; +} + +.footer-links { + margin-bottom: 3%; + text-align: left; +} + +.footer-link { + font-size: 12px; + font-weight: bold; + margin: 10px; + color: #ffffff; + text-decoration: none; +} + +.footer-link:hover { + text-decoration: underline; +} + +.footer-bottom { + display: flex; + align-items: center; + justify-content: left; +} + +.footer-logo { + width: 30px; + height: auto; + margin: 0 10px 0 -10px; +} + +.footer-text { + font-size: 12px; + color: #ffffff; + text-align: left; +} + +.footer-text p { + margin: 0; + line-height: 1.4; +} diff --git a/client/src/components/common/Footer.js b/client/src/components/common/Footer.js new file mode 100644 index 0000000..a3309cc --- /dev/null +++ b/client/src/components/common/Footer.js @@ -0,0 +1,34 @@ +import "./Footer.css"; +import React from "react"; +import { Link } from "react-router-dom"; + +const Footer = ({}) => { + return ( +
+
+
+ + 개인정보처리방침 + + + 이용약관 + + + 고객센터 + +
+ +
+ + +
+

부산광역시 남구 용소로 45, 부경대학교

+

Copyright © 2024 PNKU WAP WEP TEAM 3

+
+
+
+
+ ); +}; + +export default Footer; diff --git a/client/src/components/common/Header.css b/client/src/components/common/Header.css new file mode 100644 index 0000000..3a4d0e0 --- /dev/null +++ b/client/src/components/common/Header.css @@ -0,0 +1,78 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.Header { + display: flex; + justify-content: space-between; /* 좌우로 요소를 분배 */ + align-items: center; /* 세로 가운데 정렬 */ + position: absolute; + overflow: hidden; + flex-wrap: nowrap; /* 요소들이 줄바꿈 없이 수평으로 배치 */ + box-sizing: border-box; /* 패딩 포함 너비 조정 */ + top: 0; + left: 0; + + padding: 9px 13%; /* 상하, 좌우 여백 */ + background-color: transparent; + z-index: 100; + width: 100%; + box-sizing: border-box; /* 패딩과 보더를 포함하여 너비 계산 */ +} + +.logo-img { + /* width: 31.47px; */ + height: 51px; +} + +.login-button-section { + margin: 0; + display: flex; + justify-content: center; + white-space: nowrap; +} + +.sign-in-button { + width: 70px; + height: 30px; + top: 28px; + border-radius: 5.75px; + background-color: #ffffff; + color: black; + border: none; + font-size: 12px; + font-family: "Pretendard", sans-serif; + transition: 0.2s; +} + +.sign-in-button:hover { + cursor: pointer; + background-color: #ffffffdd; + transition: 0.2s; +} + +.nav-section { + display: flex; + flex: auto; + white-space: nowrap; +} + +.mypage-btn { + width: 34px; +} + +.logout-button { + font-family: "Pretendard", sans-serif; + background: none; + border: none; + color: black; + font-size: 12px; + text-align: center; +} + +.logout-button:hover { + cursor: pointer; + opacity: 0.8; +} diff --git a/client/src/components/common/Header.js b/client/src/components/common/Header.js new file mode 100644 index 0000000..be94c89 --- /dev/null +++ b/client/src/components/common/Header.js @@ -0,0 +1,106 @@ +import "./Header.css"; +import { Link, useLocation } from "react-router-dom"; +import Navigation from "../navSearchBar/Navigation"; +import styled from "styled-components"; +import React, { useState, useEffect } from "react"; +import logoutApi from "../../api/logoutApi.js"; + +const HeaderContainer = styled.header` + color: ${({ $textColor }) => $textColor || "rgb(236, 232, 228)"}; +`; + +const StyledButton = styled.button` + color: ${({ $textColor }) => $textColor || "rgb(0, 0, 0)"}; +`; + +const Header = ({ textColor: propTextColor, showNavigation = true }) => { + const location = useLocation(); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + // 로그인 상태 확인 + useEffect(() => { + const token = localStorage.getItem("accessToken"); + setIsLoggedIn(!!token); // 토큰이 있으면 true, 없으면 false + }, []); + + // 로그아웃 핸들러 + const handleLogout = async () => { + const token = localStorage.getItem("refreshToken"); + console.log(!!token ? "refreshToken 있음" : "refreshToken 없음"); // 확인용 상태 메시지 출력 + try { + const response = await logoutApi(token); // API 호출 + console.log(response.message); // "Successfully log out" 출력 + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + setIsLoggedIn(false); + } catch (error) { + alert("로그아웃에 실패했습니다."); + } + }; + + // 경로에 따라 Header text 색상 설정 + const getTextColor = () => { + switch (location.pathname) { + case "/fruitWine": + return "#574f4b"; + case "/cheongtakju": + return "#574f4b"; + default: + return "rgb(255, 255, 255)"; // 기본 색상 + } + }; + + // props로 받은 textColor가 없을 때만 getTextColor() 사용 + const textColor = propTextColor || getTextColor(); + + return ( + +
+ + Logo + +
+ + {showNavigation && ( +
+ +
+ )} + +
+ {/* 임시 마이페이지 버튼 */} + + 마이페이지 + + + {/* 임시 로그아웃 버튼 */} + + 로그아웃 + + + {isLoggedIn ? ( + <>{/* 연동 후 로그아웃, 마이페이지 버튼 */} + ) : ( + + + 로그인 + + + )} +
+
+ ); +}; + +export default Header; diff --git a/client/src/components/mainContents/MainContents.css b/client/src/components/mainContents/MainContents.css new file mode 100644 index 0000000..a241592 --- /dev/null +++ b/client/src/components/mainContents/MainContents.css @@ -0,0 +1,189 @@ +.MainContents { + /* min-height: 400vh; */ + width: 100%; + display: flex; + flex-direction: column; /* 세로로 쌓이도록 설정 */ + align-items: center; /* 가로 중앙 정렬 */ + z-index: -10; /* 기본 콘텐츠 위에 배경 배치 */ + + /* background-color: #f2eee7; */ + background: linear-gradient( + to right, + #f2eee7 0%, + #f2eee7 13%, + + rgba(242, 238, 231, 0.5) 13%, + rgba(242, 238, 231, 0.5) 87%, + + #f2eee7 87%, + #f2eee7 100% + ); +} + +.banner-img { + width: 100%; + height: 460px; + /* position: fixed; */ + z-index: 0; + text-align: center; + object-fit: cover; +} + +/* 검색창 */ +.search-bar { + margin-top: 50px; + padding: 25px 30px; + width: 55%; + height: 30px; + border-radius: 50px; + border: none; + background-color: #d4cdc1; + color: white; +} + +.search-bar::placeholder { + color: #ffffff; + font-size: 15px; +} + +/* 카테고리 섹션 */ +.category-section { + width: 78%; + margin: 200px 10% auto 10%; + align-items: center; + display: flex; + flex-direction: column; + gap: 30px; + padding: 20% auto; + + overflow: hidden; + /* border: 2px solid salmon; */ +} + +.category-title { + font-family: "Noto Serif KR", serif; + color: #6b5a48; + font-size: 18px; + font-weight: 600; + padding: 0; + margin-bottom: 60px; +} + +.highlight { + font-size: 24px; +} + +.category-block { + /* border: 2px solid salmon; */ + display: flex; + gap: 50px; /* 가로 배치된 아이템 간격 */ + max-width: 100%; + + /* 서브 이미지의 부모 요소 */ + position: relative; + overflow: visible; +} + +.category-block1 { + align-self: flex-start; + margin-left: 10%; +} + +.category-block2 { + align-self: flex-end; + margin-right: 8%; +} + +.category-block3 { + align-self: center; + margin-top: 30px; + margin-bottom: 10%; +} + +.cate-img1 img { + width: 380px; + height: auto; + object-fit: cover; +} + +.cate-img2 img { + width: 300px; + height: auto; + object-fit: cover; +} + +.cate-img3 img { + width: 380px; + height: auto; + object-fit: cover; +} + +.cate-text { + white-space: nowrap; + overflow: hidden; +} + +.cate-text1 { + text-align: left; + margin-top: 15%; +} + +.cate-text2 { + text-align: right; + margin-top: 13%; +} + +.cate-text3 { + text-align: left; + margin-top: 6%; + margin-left: 10px; +} + +.cate-text h3 { + font-size: 16px; + font-weight: 500; +} + +.cate-span { + font-size: 18px; + font-weight: bold; +} + +.cate-text p { + font-size: 12px; + line-height: 1.5; + margin: 20px auto; +} + +.cate-link { + font-size: 12px; + color: #ac9885; + text-decoration: underline; + text-underline-offset: 5px; +} + +/* 카테고리 서브 이미지 */ +.cate-sub { + position: absolute; +} + +.cate-sub1 { + width: 50%; + top: 30%; + right: 0; + transform: translateX(103%); +} + +.cate-sub2 { + width: 40%; + top: 60%; + left: 0; + transform: translateX(-135%); +} + +.cate-sub3 { + width: 60%; + top: 35%; + right: 0; + transform: translateX(56%); +} diff --git a/client/src/components/mainContents/MainContents.js b/client/src/components/mainContents/MainContents.js new file mode 100644 index 0000000..9c6242d --- /dev/null +++ b/client/src/components/mainContents/MainContents.js @@ -0,0 +1,109 @@ +import "./MainContents.css"; +import { Link } from "react-router-dom"; +import { useState, useEffect } from "react"; +import Footer from "../common/Footer"; + +const MainContents = () => { + return ( +
+
+ +
+ + {/* 검색창 */} + + + {/* 카테고리 섹션 */} +
+

+ 다채로운 전통주 한 잔, 홀짝 맛보는 + 즐거움 +

+ + {/* 첫 번째 블록 */} +
+
+ category-img1 +
+
+

+ 쌀과 물로 빚어낸 맑은 술, 청주 +

+

+ 빛깔이 맑고 투명해 '맑은 술'로 불리는 청주는 쌀을 발효해
+ 은은한 향과 부드러운 맛을 자랑합니다. +

+ + 더 보러가기 > + +
+ sub-category-img1 +
+ + {/* 두 번째 블록 */} +
+
+

+ 부드럽고 진한 맛, 전통의 탁주 +

+

+ 쌀과 누룩으로 만든 걸쭉한 전통주인 탁주는 고소하고 깊은 맛이 + 특징입니다.
+ 신선한 발효 향이 어우러져 풍미가 깊습니다. +

+ + 더 보러가기 > + +
+
+ category-img2 +
+ sub-category-img2 +
+ + {/* 세 번째 블록 */} +
+
+ category-img3 +
+
+

+ 과일의 향과 맛을 담은 술, + 과실주 +

+

+ 매실, 복숭아, 오미자 등 다양한 과일로 만든 전통주인 과실주는 + 과일의 상큼한 향과
+ 맛을 즐길 수 있습니다. +

+ + 더 보러가기 > + +
+ sub-category-img3 +
+
+
+ ); +}; + +export default MainContents; diff --git a/client/src/components/navSearchBar/Navigation.css b/client/src/components/navSearchBar/Navigation.css new file mode 100644 index 0000000..f0a1a70 --- /dev/null +++ b/client/src/components/navSearchBar/Navigation.css @@ -0,0 +1,25 @@ +nav { + display: flex; /* Flexbox로 가로 배치 */ + /* justify-content: space-around; */ + align-items: center; /* 세로 중앙 정렬 */ +} + +nav div { + margin: 0 10px; /* 좌우에 여유 공간 */ +} + +.nav-line { + color: #d9d9d9; + z-index: 3; +} + +nav .link { + text-decoration: none; + font-size: 20px; + /* color: white; */ +} + +nav .link-fruit { + /* margin: auto 41px auto 113.77px; */ + margin-left: 70px; +} diff --git a/client/src/components/navSearchBar/Navigation.js b/client/src/components/navSearchBar/Navigation.js new file mode 100644 index 0000000..be8cba2 --- /dev/null +++ b/client/src/components/navSearchBar/Navigation.js @@ -0,0 +1,37 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { styled } from "styled-components"; +import "./Navigation.css"; + +const StyledLink = styled(Link)` + color: ${({ textColor }) => textColor || "inherit"}; + text-decoration: none; +`; + +function Navigation({ textColor }) { + return ( + + ); +} + +export default Navigation; diff --git a/client/src/components/navSearchBar/SearchBar.css b/client/src/components/navSearchBar/SearchBar.css new file mode 100644 index 0000000..05d4200 --- /dev/null +++ b/client/src/components/navSearchBar/SearchBar.css @@ -0,0 +1,35 @@ +.SearchBar { + display: flex; + justify-content: center; /* 수평 중앙 정렬 */ + align-items: center; /* 수직 중앙 정렬 (필요시 상위 컨테이너에 적용) */ + margin: 5% 0; /* 페이지 상단으로부터 간격 */ + flex-wrap: nowrap; +} + +.SearchBar input { + width: 100%; /* 화면 크기에 맞게 너비 설정 */ + max-width: 500px; /* 최대 너비 제한 */ + padding: 10px 15px; /* 여백 설정 */ + font-size: 15px; + border: 1px solid #ccc; + border-radius: 20px; +} + +input::placeholder { + color: rgb(73, 73, 73); + opacity: 0.8; + font-size: 13px; +} + +input:focus { + outline: none; + box-shadow: none; +} + +/* .search-icon { + width: 20px; + height: 20px; + cursor: pointer; + margin-left: auto; + opacity: 0.85; +} */ diff --git a/client/src/components/navSearchBar/SearchBar.js b/client/src/components/navSearchBar/SearchBar.js new file mode 100644 index 0000000..e30f225 --- /dev/null +++ b/client/src/components/navSearchBar/SearchBar.js @@ -0,0 +1,12 @@ +import "./SearchBar.css"; + +const SearchBar = () => { + return ( +
+ {/* */} + +
+ ); +}; + +export default SearchBar; diff --git a/client/src/components/search/search.css b/client/src/components/search/search.css new file mode 100644 index 0000000..2552c58 --- /dev/null +++ b/client/src/components/search/search.css @@ -0,0 +1,29 @@ +/* 페이지 전체 레이아웃 */ +.search-container { + display: flex; + align-items: center; + justify-content: center; /* 수평 중앙 정렬 */ + background-color: #d6d1c9; + border-radius: 20px; + padding: 10px 15px; + max-width: 600px; + margin-top: 90px; + margin-left: auto; + margin-right: auto; /* 좌우 중앙 정렬 */ +} + +.search-input { + flex: 1; + border: none; + outline: none; + background-color: transparent; + font-size: 16px; + color: #333; +} + +/* 검색 아이콘 스타일 */ +.search-icon { + width: 20px; + height: 20px; + cursor: pointer; +} diff --git a/client/src/components/search/search.js b/client/src/components/search/search.js new file mode 100644 index 0000000..0a41ad4 --- /dev/null +++ b/client/src/components/search/search.js @@ -0,0 +1,49 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import "./search.css"; + +function SearchPage() { + const [searchQuery, setSearchQuery] = useState(""); + const [data, setData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + + // 데이터 가져오기 + useEffect(() => { + axios + .get("/data/drinks.json") // JSON 파일의 경로로 대체 + .then(response => setData(response.data)) + .catch(error => console.error("Error fetching data:", error)); + }, []); + + const handleSearch = () => { + const results = data.filter(item => + item.drinkName && item.drinkName.toLowerCase().includes(searchQuery.toLowerCase()) + ); + setFilteredData(results); + }; + + return ( +
+ setSearchQuery(e.target.value)} + className="search-input" + /> + search icon +
    + {filteredData.map(item => ( +
  • {item.name}
  • + ))} +
+
+ ); +} + +export default SearchPage; diff --git a/client/src/components/signIn/SignInContent.css b/client/src/components/signIn/SignInContent.css new file mode 100644 index 0000000..33c724e --- /dev/null +++ b/client/src/components/signIn/SignInContent.css @@ -0,0 +1,84 @@ +.signin-background { + display: flex; + align-items: center; + justify-content: center; + background-color: #f0f8ff; + height: 80vh; + margin-top: 61px; +} + +.signin-container { + display: flex; + width: 80%; + max-width: 800px; + height: 400px; + background-color: #f5f5f5; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.signin-left { + width: 40%; + background-color: #ffffff; +} + +.signin-right { + width: 60%; + display: flex; + align-items: center; + justify-content: center; +} + +.signin-form { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + width: 80%; +} + +.signin-input { + padding: 12px; + border-radius: 25px; + font-size: 16px; + color: black; + background-color: #fff; + font-weight: 700; + width: 100%; + border: none; +} + +.signin-input::placeholder { + color: black; +} + +.signin-button { + width: 150px; + padding: 5px; + background-color: #fff; + border-radius: 25px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.3s; + display: inline-flex; + justify-content: center; + font-weight: 700; +} + +.signin-button:hover { + background-color: #bbb; +} + +.signin-signup-button { + width: 100%; + display: inline-flex; + justify-content: flex-end; + font-size: 10px; + color: gray; +} + +.signin-signup-button a { + color: inherit; + text-decoration: none; +} diff --git a/client/src/components/signIn/SignInContent.js b/client/src/components/signIn/SignInContent.js new file mode 100644 index 0000000..76f4dfb --- /dev/null +++ b/client/src/components/signIn/SignInContent.js @@ -0,0 +1,31 @@ +import React from "react"; +import "./SignInContent.css"; +import { Link } from "react-router-dom"; + +const SignInContent = () => { + return ( +
+
+
+
+
+ + + +
+ 회원가입 +
+ +
로그인
+
+
+
+
+ ); +}; + +export default SignInContent; \ No newline at end of file diff --git a/client/src/components/signUp/SignUpContent.css b/client/src/components/signUp/SignUpContent.css new file mode 100644 index 0000000..615a38c --- /dev/null +++ b/client/src/components/signUp/SignUpContent.css @@ -0,0 +1,80 @@ +.signup-background { + display: flex; + align-items: center; + justify-content: center; + background-color: #f0f8ff; + height: 80vh; + margin-top: 61px; +} + +.signup-container { + display: flex; + width: 80%; + max-width: 800px; + height: 400px; + background-color: #f5f5f5; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.signup-left { + width: 40%; + background-color: #ffffff; +} + +.signup-right { + width: 60%; + display: flex; + align-items: center; + justify-content: center; +} + +.signup-form { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + width: 80%; + min-height: 300px; +} + +.signup-input { + padding: 8px; + border-radius: 25px; + font-size: 16px; + color: black; + background-color: #fff; + font-weight: 700; + width: 100%; + border: none; +} + +.signup-input::placeholder { + color: black; +} + +.error-text { + color: red; + font-size: 8px; + margin-top: -18px; + align-self: flex-start; +} + +.signup-button { + margin-top: 15px; + width: 150px; + padding: 5px; + background-color: #fff; + border-radius: 25px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.3s; + display: inline-flex; + justify-content: center; + font-weight: 700; +} + +.signup-button:hover { + background-color: #bbb; +} diff --git a/client/src/components/signUp/SignUpContent.js b/client/src/components/signUp/SignUpContent.js new file mode 100644 index 0000000..edc4788 --- /dev/null +++ b/client/src/components/signUp/SignUpContent.js @@ -0,0 +1,110 @@ +import React, { useState } from "react"; +import "./SignUpContent.css"; +import Header from "../common/Header"; + +const SignUpContent = () => { + const [id, setId] = useState(""); + const [pw, setPw] = useState(""); + const [pwCheck, setPwCheck] = useState(""); + const [nickname, setNickname] = useState(""); + + const [errors, setErrors] = useState({ + idError: "", + pwError: "", + pwCheckError: "", + }); + + const validateForm = () => { + const newErrors = { + idError: id.includes("@") ? "" : "@ 가 포함되어야 합니다.", + pwError: pw.length >= 8 ? "" : "8자리 이상 입력해주세요.", + pwCheckError: pw === pwCheck ? "" : "비밀번호와 일치하지 않습니다.", + }; + setErrors(newErrors); + + // 모든 입력이 올바를 때만 true 반환 + return !newErrors.idError && !newErrors.pwError && !newErrors.pwCheckError; + }; + + const handleSubmit = () => { + if (validateForm()) { + // 폼이 유효할 때만 회원가입 로직을 수행 + console.log("회원가입 완료!"); + } + }; + + const handleIdChange = (e) => { + setId(e.target.value); + if (errors.idError) { + setErrors((prevErrors) => ({ ...prevErrors, idError: "" })); + } + }; + + const handlePwChange = (e) => { + setPw(e.target.value); + if (errors.pwError) { + setErrors((prevErrors) => ({ ...prevErrors, pwError: "" })); + } + }; + + const handlePwCheckChange = (e) => { + setPwCheck(e.target.value); + if (errors.pwCheckError) { + setErrors((prevErrors) => ({ ...prevErrors, pwCheckError: "" })); + } + }; + + return ( +
+
+
+
+
+
+ + {errors.idError && ( +
{errors.idError}
+ )} + + + {errors.pwError && ( +
{errors.pwError}
+ )} + + + {errors.pwCheckError && ( +
{errors.pwCheckError}
+ )} + + setNickname(e.target.value)} + /> + +
+ 회원가입 완료 +
+
+
+
+
+ ); +}; + +export default SignUpContent; diff --git a/client/src/hooks/useScrollFadeIn.js b/client/src/hooks/useScrollFadeIn.js new file mode 100644 index 0000000..2fb9b4f --- /dev/null +++ b/client/src/hooks/useScrollFadeIn.js @@ -0,0 +1,33 @@ +import { useState, useEffect, useRef } from "react"; + +function useScrollFadeIn() { + const [isVisible, setIsVisible] = useState(false); + const ref = useRef(); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.1 } + ); + + if (ref.current) observer.observe(ref.current); + + return () => { + if (ref.current) observer.unobserve(ref.current); + }; + }, []); + + const motionProps = { + initial: { opacity: 0, y: 50 }, + animate: isVisible ? { opacity: 1, y: 0 } : {}, + transition: { duration: 1, ease: "easeOut" }, + }; + + return [ref, motionProps]; +} + +export default useScrollFadeIn; diff --git a/client/src/pages/CheongtakjuPage.js b/client/src/pages/CheongtakjuPage.js new file mode 100644 index 0000000..0eb2118 --- /dev/null +++ b/client/src/pages/CheongtakjuPage.js @@ -0,0 +1,19 @@ +import Header from "../components/common/Header"; +import AlcoholList from "../components/alcoholList/AlcoholList"; +import SearchBar from "../components/navSearchBar/SearchBar"; +import Footer from "../components/common/Footer"; +import cheongTakjuListApi from "../api/cheongTakjuListApi"; +import SearchPage from "../components/search/search"; + +const CheongtakjuPage = () => { + return ( +
+
+ + +
+
+ ); +}; + +export default CheongtakjuPage; diff --git a/client/src/pages/FruitWinePage.js b/client/src/pages/FruitWinePage.js new file mode 100644 index 0000000..33643e2 --- /dev/null +++ b/client/src/pages/FruitWinePage.js @@ -0,0 +1,19 @@ +import Header from "../components/common/Header"; +import SearchBar from "../components/navSearchBar/SearchBar"; +import AlcoholList from "../components/alcoholList/AlcoholList"; +import fruitWineListApi from "../api/fruitWineListApi"; +import Footer from "../components/common/Footer"; +import SearchPage from "../components/search/search"; + +const FruitWinePage = () => { + return ( +
+
+ + +
+
+ ); +}; + +export default FruitWinePage; diff --git a/client/src/pages/MainPage.js b/client/src/pages/MainPage.js index e69de29..a0cbe38 100644 --- a/client/src/pages/MainPage.js +++ b/client/src/pages/MainPage.js @@ -0,0 +1,15 @@ +import Header from "../components/common/Header"; +import Footer from "../components/common/Footer"; +import MainContents from "../components/mainContents/MainContents"; + +const MainPage = () => { + return ( +
+
+ +
+
+ ); +}; + +export default MainPage; diff --git a/client/src/pages/SignInPage.js b/client/src/pages/SignInPage.js new file mode 100644 index 0000000..3c3d124 --- /dev/null +++ b/client/src/pages/SignInPage.js @@ -0,0 +1,15 @@ +import Header from "../components/common/Header"; +import Footer from "../components/common/Footer"; +import SignInContent from "../components/signIn/SignInContent"; + +const SignInPage = () => { + return ( +
+
+ +
+
+ ); +}; + +export default SignInPage; diff --git a/client/src/pages/SignUpPage.js b/client/src/pages/SignUpPage.js new file mode 100644 index 0000000..3ea14ba --- /dev/null +++ b/client/src/pages/SignUpPage.js @@ -0,0 +1,15 @@ +import Header from "../components/common/Header"; +import Footer from "../components/common/Footer"; +import SignUpContent from "../components/signUp/SignUpContent"; + +const SignUpPage = () => { + return ( +
+
+ +
+
+ ); +}; + +export default SignUpPage; \ No newline at end of file diff --git a/client/src/pages/mypage.css b/client/src/pages/mypage.css new file mode 100644 index 0000000..958ffa7 --- /dev/null +++ b/client/src/pages/mypage.css @@ -0,0 +1,185 @@ +/* 전체 페이지 레이아웃 설정 */ +.profile-page { + display: flex; + justify-content: space-between; + font-family: "Arial", sans-serif; + + margin-top: 4%; +} + +/* 좌측 프로필 섹션 */ +.profile-left { + padding: 60px; + width: 30%; + display: flex; + flex-direction: column; + align-items: center; +} + +.profile-picture { + width: 150px; + height: 150px; + background-color: #d9d9d9; + border-radius: 50%; + overflow: hidden; + margin-bottom: 20px; +} + +.profile-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-info { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.edit-btn { + margin-top: 10px; + padding: 8px 16px; + background-color: white; + color: black; + border: none; + border-radius: 5px; + cursor: pointer; + text-decoration: underline; +} + +.button-container { + display: flex; /* 버튼을 가로로 정렬 */ + gap: 20px; /* 버튼 간격 */ + margin-top: 10px; /* 버튼과 입력 필드 사이의 여백 */ +} + +.save-btn { + margin-top: 10px; + padding: 8px 16px; + background-color: white; + color: black; + border: none; + cursor: pointer; + text-decoration: underline; +} + +/* 우측 취향 섹션 */ +.profile-right { + width: 65%; +} + +.preference-section { + background-color: #f9f9f9; + padding: 50px; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.preference-section h3 { + margin-bottom: 60px; +} + +.preference-details { + display: flex; + flex-direction: column; + justify-content: space-start; +} + +.preference-score { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.preference-input { + border: none; /* 테두리 제거 */ + padding: 5px; /* 안쪽 여백 */ + width: 30px; /* 너비 */ + margin-left: 5px; /* 왼쪽 여백 */ + outline: none; /* 포커스 시 테두리도 제거 */ + background-color: #f6f6f6; + text-align: right; + font-size: 16px; /* 텍스트 입력 크기 */ +} + +.percentage { + font-size: 16px !important; /* 입력 텍스트와 동일한 크기 */ +} + +.preference-score span:first-child { + font-weight: bold; +} + +.preference-score span:last-child { + font-size: 24px; + color: #555; +} + +/* 담은 술 섹션 */ +.favorite-alcohol h4 { + margin-bottom: 10px; +} + +.add-btn{ + border: none; + margin-bottom: 30px; + cursor: pointer; +} + +.alcohol-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); /* 4개의 열로 설정 */ + grid-template-rows: repeat(2, 1fr); /* 2개의 행으로 설정 */ + gap: 20px; /* 간격을 20px로 설정 */ + margin-bottom: 30px; +} + +.alcohol-item { + width: 170px; + height: 165px; + background-color: #e0e0e0; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + color: #888; +} + +/* .alcohol-item.add-more { + background-color: #d0d0d0; + font-weight: bold; + cursor: pointer; +} +*/ + +.alcohol-item.empty { + background-color: #f0f0f0; +} + +/* 닉네임 변경 */ +.nickname-container { + margin-bottom: 10px; /* 닉네임과 버튼 사이의 간격 설정 */ + display: flex; /* 수평 정렬을 위한 플렉스 사용 */ + align-items: center; /* 세로 중앙 정렬 */ +} + +.nickname-input { + font-size: 18px; /* 글자 크기 설정 */ + padding: 5px; /* 여백 설정 */ + border: none; + width: 100%; /* 전체 너비 사용, 여백을 고려 */ + text-align: center; /* 아래 여백 설정 */ +} + +.profile-info h2 { + margin: 0; /* 기본 마진 제거 */ +} + +.nickname-error { + color: red; + font-size: 12px; + margin-top: 5px; +} \ No newline at end of file diff --git a/client/src/pages/mypage.js b/client/src/pages/mypage.js new file mode 100644 index 0000000..52b860a --- /dev/null +++ b/client/src/pages/mypage.js @@ -0,0 +1,164 @@ +import React, { useState, useRef } from "react"; +import "./mypage.css"; // CSS 파일을 불러옵니다 +import Header from "../components/common/Header"; +import { useNavigate } from "react-router-dom"; + +function Mypage() { + const [isEditing, setIsEditing] = useState(false); // 편집 모드 상태 + const [nickname, setNickname] = useState("Nickname"); // 닉네임 상태 + const [nicknameError, setNicknameError] = useState(""); // 닉네임 오류 메시지 상태 + const [preferenceScore, setPreferenceScore] = useState(""); // 선호도수 상태 + const [profileImage, setProfileImage] = useState("default-avatar.png"); // 프로필 이미지 상태 + + + const fileInputRef = useRef(null); // 파일 입력을 위한 ref 생성 + const navigate = useNavigate(); + + const handleEditClick = () => { + setIsEditing(true); // 편집 모드로 변경 + setNicknameError(""); // 편집 모드 들어갈 때 오류 메시지 초기화 + }; + + const handleSaveClick = () => { + setIsEditing(false); // 편집 모드 해제 + }; + + const handlePreferenceChange = (e) => { + // 입력된 값이 숫자만 포함된 값으로 업데이트 + const value = e.target.value; + + if (/^\d*$/.test(value)) { + setPreferenceScore(value); // 숫자만 입력된 경우 상태 업데이트 + } +}; + + const handleNicknameChange = (e) => { + const newNickname = e.target.value; // 닉네임 상태 업데이트 + if (newNickname.length <= 10) { + setNickname(newNickname); // 닉네임 상태 업데이트 + setNicknameError(""); // 오류 메시지 초기화 + } else { + setNicknameError("글자수가 초과되었습니다"); // 글자수가 초과된 경우 오류 메시지 설정 + } + }; + + const handleImageChange = (e) => { + const file = e.target.files[0]; // 선택된 파일 + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setProfileImage(reader.result); // 파일을 읽고 이미지로 설정 + }; + reader.readAsDataURL(file); // 파일을 데이터 URL로 읽기 + } + }; + + const handleDeleteClick = () => { + setProfileImage("default-avatar.png"); // 기본 프로필 이미지로 설정 + }; + + const openFileDialog = () => { + fileInputRef.current.click(); // 파일 입력 클릭 + }; + + const handleNicknameFocus = () => { + setNickname(""); // 입력란 클릭 시 빈 칸으로 설정 + }; + + const handleAddAlcoholClick=()=>{ + navigate("/cheongtakju"); + }; + + + return ( +
+
+
+
+ +
+
+ {isEditing ? ( + <> + + {nicknameError &&

{nicknameError}

} {/* 오류 메시지 표시 */} +
+ + +
+ + + + ) : ( + <> +

{nickname}

+ + + )} +
+
+ +
+
+

내 취향

+
+ +
+ 선호 도수 + + % +
+ +
+

담은 술

+ +
+
+
+
+
+
+ {/* 두 번째 줄 */} +
+
+
+
+
+
+
+
+
+ ); +} + +export default Mypage;