diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a145f93c..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml new file mode 100644 index 00000000..5d68986b --- /dev/null +++ b/.github/workflows/cd-prod.yml @@ -0,0 +1,62 @@ +name: CD - PROD + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + + - uses: gradle/actions/setup-gradle@v3 + + - name: Grant execute permission + run: chmod +x gradlew + + - name: Build JAR + run: ./gradlew build -x test + + - name: Docker Login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/realmatch-backend:prod + ${{ secrets.DOCKERHUB_USERNAME }}/realmatch-backend:${{ github.sha }} + + - name: Deploy + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.PROD_SERVER_IP }} + username: ubuntu + key: ${{ secrets.PROD_SERVER_SSH_KEY }} + script: | + cd /home/ubuntu/realmatch + git pull origin main + + docker compose pull + docker compose up -d + + echo "Waiting for app to start..." + sleep 10 + + if ! docker compose ps | grep "Up"; then + echo "❌ Container is not running" + docker compose logs + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index fba7594f..5b5dbb93 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,6 +8,29 @@ jobs: build-and-test: runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_DATABASE: test_db + MYSQL_USER: test_user + MYSQL_PASSWORD: test + MYSQL_ROOT_PASSWORD: test_root + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ptest_root" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + + env: + SPRING_PROFILES_ACTIVE: test + JWT_SECRET: pPmJ9ViYt8f6HAh2q5s36QmEUeyEFRcquPaNpnIGUK8er5DjfTKa4xbDsTFXQ7HRVfTLR2DIYs7s9iGdJ+Yb7Q== + JWT_ACCESS_EXPIRE_MS: 600000 + JWT_REFRESH_EXPIRE_MS: 1209600000 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -19,6 +42,9 @@ jobs: distribution: 'temurin' cache: gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -33,12 +59,38 @@ jobs: run: ./gradlew test continue-on-error: true - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - files: | - build/test-results/**/*.xml + - name: Test application startup + run: | + ./gradlew bootRun > bootrun.log 2>&1 & + APP_PID=$! + echo "Started bootRun with PID: $APP_PID" + + # 최대 60초 대기하면서 애플리케이션 시작 확인 + for i in {1..60}; do + # 로그에서 시작 완료 메시지 확인 + if grep -q "Started RealMatchApplication" bootrun.log; then + echo "✅ Application started successfully after ${i}s" + kill $APP_PID 2>/dev/null || true + exit 0 + fi + + # 프로세스가 죽었는지 확인 + if ! kill -0 $APP_PID 2>/dev/null; then + echo "❌ Application process terminated unexpectedly" + echo "=== bootRun log ===" + cat bootrun.log + exit 1 + fi + + sleep 1 + done + + echo "❌ Application failed to start within 60 seconds" + echo "=== bootRun log ===" + cat bootrun.log + kill $APP_PID 2>/dev/null || true + exit 1 + timeout-minutes: 2 - name: Upload build artifacts if: failure() diff --git a/.gitignore b/.gitignore index e63e239a..45116f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ # 특정 파일 제외 -src/main/resources/application-*.yml +#src/main/resources/application-*.yml +src/main/resources/application.yml +src/test/resources/application.yml +src/test/resources/application-*.yml + +# 환경 변수 +.env +.env.* ### Gradle ### .gradle/ @@ -37,3 +44,8 @@ Thumbs.db ### Log files ### *.log logs/ + +### mysql ### +mysql_data/ + +bin/ \ No newline at end of file diff --git a/Docker/.DS_Store b/Docker/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/Docker/.DS_Store and /dev/null differ diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 828c250d..d0676913 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -5,6 +5,8 @@ WORKDIR /app ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar -EXPOSE 8080 +EXPOSE 6000 -ENTRYPOINT ["java", "-jar", "app.jar"] +ENV SERVER_PORT=6000 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 2e13a235..83815dc8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ git config core.hooksPath .githooks ``` +아래 명령어를 이용해서 코드 스타일을 준수했는지 확인해주세요 +```bash +./gradlew checkstyleMain checkstyleTest +``` + 아래 명령어를 통해 쉽게 도커 컨테이너에서 사용할 수 있습니다. ```bash diff --git a/build.gradle b/build.gradle index 00abbc1c..f490a7e2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.9' id 'io.spring.dependency-management' version '1.1.7' + id 'checkstyle' } group = 'com.example' @@ -25,16 +26,55 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Test dependencies + testRuntimeOnly 'com.h2database:h2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + // mySQL + runtimeOnly 'com.mysql:mysql-connector-j' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' } tasks.named('test') { useJUnitPlatform() } + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +checkstyle { + toolVersion = '10.12.5' + configFile = file("${rootProject.projectDir}/config/checkstyle/checkstyle.xml") + maxWarnings = 0 + ignoreFailures = false +} + +tasks.withType(Checkstyle) { + reports { + xml.required = true + html.required = true + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 90933b10..a2d2f9df 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,10 +6,10 @@ services: container_name: mysql_db restart: always environment: - MYSQL_DATABASE: myapp_db - MYSQL_USER: admin - MYSQL_PASSWORD: secret - MYSQL_ROOT_PASSWORD: rootsecret + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} ports: - "3306:3306" volumes: @@ -30,16 +30,17 @@ services: dockerfile: Docker/Dockerfile container_name: spring_app ports: - - "8080:8080" + - "6000:6000" depends_on: - db - redis + env_file: + - .env environment: - - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul - - SPRING_DATASOURCE_USERNAME=admin - - SPRING_DATASOURCE_PASSWORD=secret - - SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver - - SPRING_REDIS_HOST=redis - - SPRING_REDIS_PORT=6379 - - SPRING_PROFILES_ACTIVE=dev - + SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} + SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} + SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + SPRING_DATASOURCE_DRIVER_CLASS_NAME: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME} + SPRING_REDIS_HOST: ${SPRING_REDIS_HOST} + SPRING_REDIS_PORT: ${SPRING_REDIS_PORT} + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} diff --git a/src/main/java/com/example/RealMatch/RealMatchApplication.java b/src/main/java/com/example/RealMatch/RealMatchApplication.java index 4c59c599..97becbf2 100644 --- a/src/main/java/com/example/RealMatch/RealMatchApplication.java +++ b/src/main/java/com/example/RealMatch/RealMatchApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class RealMatchApplication { - public static void main(String[] args) { - SpringApplication.run(RealMatchApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(RealMatchApplication.class, args); + } } diff --git a/src/main/java/com/example/RealMatch/brand/application/service/.keep b/src/main/java/com/example/RealMatch/brand/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/domain/entity/.keep b/src/main/java/com/example/RealMatch/brand/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/domain/repository/.keep b/src/main/java/com/example/RealMatch/brand/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/presentation/controller/.keep b/src/main/java/com/example/RealMatch/brand/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/brand/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/brand/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/application/service/.keep b/src/main/java/com/example/RealMatch/campaign/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/domain/entity/.keep b/src/main/java/com/example/RealMatch/campaign/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/domain/repository/.keep b/src/main/java/com/example/RealMatch/campaign/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/presentation/controller/.keep b/src/main/java/com/example/RealMatch/campaign/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/campaign/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/campaign/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/application/service/.keep b/src/main/java/com/example/RealMatch/chat/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/domain/entity/.keep b/src/main/java/com/example/RealMatch/chat/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/domain/repository/.keep b/src/main/java/com/example/RealMatch/chat/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/presentation/controller/.keep b/src/main/java/com/example/RealMatch/chat/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/chat/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/chat/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/global/common/BaseEntity.java b/src/main/java/com/example/RealMatch/global/common/BaseEntity.java new file mode 100644 index 00000000..101d8491 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/common/BaseEntity.java @@ -0,0 +1,24 @@ +package com.example.RealMatch.global.common; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public abstract class BaseEntity { + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + +} diff --git a/src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java b/src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java new file mode 100644 index 00000000..a7bc00bd --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java @@ -0,0 +1,42 @@ +package com.example.RealMatch.global.common; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class UpdateBaseEntity extends BaseEntity { + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted; + + protected UpdateBaseEntity() { + super(); + this.isDeleted = false; + } + + public void softDelete(Integer deletedBy, LocalDateTime deletedAt) { + this.isDeleted = true; + this.deletedAt = deletedAt; + } + + public void restore() { + this.isDeleted = false; + this.deletedAt = null; + } +} diff --git a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java new file mode 100644 index 00000000..c1bfe019 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java @@ -0,0 +1,76 @@ +package com.example.RealMatch.global.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import com.example.RealMatch.global.config.jwt.JwtAuthenticationFilter; +import com.example.RealMatch.global.presentation.advice.CustomAccessDeniedHandler; +import com.example.RealMatch.global.presentation.advice.CustomAuthEntryPoint; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + private static final String[] PERMIT_ALL_URL_ARRAY = { + "/api/test", + "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html" + }; + + private static final String[] REQUEST_AUTHENTICATED_ARRAY = { + "/api/test-auth" + }; + + @Value("${cors.allowed-origin}") + private String allowedOrigin; + @Value("${swagger.server-url}") + String swaggerUrl; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthEntryPoint customAuthEntryPoint, CustomAccessDeniedHandler customAccessDeniedHandler) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthEntryPoint) // 401 + .accessDeniedHandler(customAccessDeniedHandler) // 403 + ) + + .authorizeHttpRequests(auth -> auth + .requestMatchers(REQUEST_AUTHENTICATED_ARRAY).authenticated() + .requestMatchers(PERMIT_ALL_URL_ARRAY).permitAll() + .anyRequest().denyAll() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of(allowedOrigin, "http://localhost:8080", swaggerUrl)); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); // 쿠키/인증정보 포함 요청 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} + diff --git a/src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java b/src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java new file mode 100644 index 00000000..258b3553 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java @@ -0,0 +1,52 @@ +package com.example.RealMatch.global.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; + +@Configuration +public class SwaggerConfig { + + @Value("${swagger.server-url}") + private String swaggerUrl; + + @Bean + public OpenAPI localOpenAPI() { + Info info = new Info() + .title("🔗 RealMatch API") + .version("1.0.0") + .description("RealMatch API 명세서입니다."); + + String jwtSchemeName = "JWT Authentication"; + + io.swagger.v3.oas.models.security.SecurityScheme securityScheme = new io.swagger.v3.oas.models.security.SecurityScheme() + .name("Authorization") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"); + + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, securityScheme); + + return new OpenAPI() + .info(info) + .addSecurityItem(securityRequirement) + .components(components) + .servers(List.of( + new Server() + .url(swaggerUrl) + )); + } + +} diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java b/src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java new file mode 100644 index 00000000..4f6af748 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java @@ -0,0 +1,58 @@ +package com.example.RealMatch.global.config.jwt; + +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.Getter; + +@Getter +public class CustomUserDetails implements UserDetails { + + private final Long userId; // DB PK + private final String providerId; // 소셜 고유 ID + private final String role; // USER / ADMIN + + public CustomUserDetails(Long userId, String providerId, String role) { + this.userId = userId; + this.providerId = providerId; + this.role = role; + } + + @Override + public Collection getAuthorities() { + return List.of(() -> "ROLE_" + role); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return providerId; + } // 소셜 UUID 기준 + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..37964ae8 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,62 @@ +package com.example.RealMatch.global.config.jwt; + +import java.io.IOException; + +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + + if (!jwtProvider.validateToken(token)) { + filterChain.doFilter(request, response); + return; + } + + Long userId = jwtProvider.getUserId(token); + String providerId = jwtProvider.getProviderId(token); + String role = jwtProvider.getRole(token); + + CustomUserDetails userDetails = + new CustomUserDetails(userId, providerId, role); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + filterChain.doFilter(request, response); + } + +} diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java b/src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java new file mode 100644 index 00000000..9071df96 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java @@ -0,0 +1,101 @@ +package com.example.RealMatch.global.config.jwt; + +import java.util.Base64; +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@Component +public class JwtProvider { + + private final SecretKey secretKey; + private final long accessTokenExpireMillis; + private final long refreshTokenExpireMillis; + + public JwtProvider( + @Value("${jwt.secret}") String secret, + @Value("${jwt.access-expire-ms}") long accessTokenExpireMillis, + @Value("${jwt.refresh-expire-ms}") long refreshTokenExpireMillis + ) { + byte[] keyBytes = Base64.getDecoder().decode(secret); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + this.accessTokenExpireMillis = accessTokenExpireMillis; + this.refreshTokenExpireMillis = refreshTokenExpireMillis; + } + + // ========================= + // 토큰 생성 + // ========================= + public String createAccessToken(Long userId, String providerId, String role) { + return createToken(userId, providerId, role, accessTokenExpireMillis); + } + + public String createRefreshToken(Long userId, String providerId, String role) { + return createToken(userId, providerId, role, refreshTokenExpireMillis); + } + + private String createToken(Long userId, String providerId, String role, long expireMillis) { + long now = System.currentTimeMillis(); + + return Jwts.builder() + .subject(String.valueOf(userId)) + .claim("providerId", providerId) + .claim("role", role) + .issuedAt(new Date(now)) + .expiration(new Date(now + expireMillis)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + // ========================= + // 토큰 검증 + // ========================= + public boolean validateToken(String token) { + try { + parseClaims(token); + return true; + } catch (ExpiredJwtException e) { + return false; // 만료 + } catch (JwtException | IllegalArgumentException e) { + return false; // 변조, 손상 등 + } + } + + // ========================= + // Claims 가져오기 + // ========================= + public Claims getClaims(String token) { + return parseClaims(token).getPayload(); + } + + public Long getUserId(String token) { + return Long.parseLong(getClaims(token).getSubject()); + } + + public String getProviderId(String token) { + return getClaims(token).get("providerId", String.class); + } + + public String getRole(String token) { + return getClaims(token).get("role", String.class); + } + + private Jws parseClaims(String token) { + return Jwts.parser() + .verifyWith(secretKey) // Key 타입 강제 + .build() + .parseSignedClaims(token); + } +} + diff --git a/src/main/java/com/example/RealMatch/global/controller/TestController.java b/src/main/java/com/example/RealMatch/global/controller/TestController.java new file mode 100644 index 00000000..3ee96566 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/controller/TestController.java @@ -0,0 +1,58 @@ +package com.example.RealMatch.global.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.RealMatch.global.config.jwt.CustomUserDetails; +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralSuccessCode; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "test", description = "테스트용 API") +@RestController +@RequestMapping("/api") +public class TestController { + + @Operation(summary = "api 테스트 확인", + description = """ + 테스트용 api입니다. + 만약 이 api가 통과하지 않는다면, SecurityConfig에 url을 추가해야합니다. + + 인증이 필요없다면, PERMIT_ALL_URL_ARRAY에 추가하고, + 인증이 필요하다면, REQUEST_AUTHENTICATED_ARRAY에 추가해주세요. + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "테스트 성공") + }) + @GetMapping("/test") + public CustomResponse test() { + String response = "Hello from Spring Boot 👋"; + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, response); + } + + @Operation(summary = "api 권한 테스트 확인", + description = """ + 테스트용 api입니다. + Swagger에서 Authorize에 토큰을 입력한 후 사용해야 정상 작동합니다. + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "테스트 성공"), + @ApiResponse(responseCode = "COMMON401_1", description = "인증이 필요합니다.") + }) + @GetMapping("/test-auth") + public CustomResponse testAuth( + @AuthenticationPrincipal CustomUserDetails user + ) { + String response = "Hello from Spring Boot 👋"; + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, response); + } + + +} + diff --git a/src/main/java/com/example/RealMatch/global/presentation/CustomResponse.java b/src/main/java/com/example/RealMatch/global/presentation/CustomResponse.java new file mode 100644 index 00000000..af6f39ae --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/CustomResponse.java @@ -0,0 +1,41 @@ +package com.example.RealMatch.global.presentation; + +import com.example.RealMatch.global.presentation.code.BaseErrorCode; +import com.example.RealMatch.global.presentation.code.BaseSuccessCode; +import com.example.RealMatch.global.presentation.code.GeneralSuccessCode; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class CustomResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + + @JsonProperty("code") + private final String code; + + @JsonProperty("message") + private final String message; + + @JsonProperty("result") + private T result; + + // 200 OK + public static CustomResponse ok(T result) { + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, result); + } + + public static CustomResponse onSuccess(BaseSuccessCode code, T result) { + return new CustomResponse<>(true, code.getCode(), code.getMessage(), result); + } + + public static CustomResponse onFailure(BaseErrorCode code, T result) { + return new CustomResponse<>(false, code.getCode(), code.getMessage(), result); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..012d123c --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java @@ -0,0 +1,42 @@ +package com.example.RealMatch.global.presentation.advice; + +import java.io.IOException; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + + response.setStatus(GeneralErrorCode.FORBIDDEN.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + + CustomResponse body = + CustomResponse.onFailure( + GeneralErrorCode.FORBIDDEN, + null + ); + + response.getWriter() + .write(objectMapper.writeValueAsString(body)); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java new file mode 100644 index 00000000..79fdc7ed --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java @@ -0,0 +1,42 @@ +package com.example.RealMatch.global.presentation.advice; + +import java.io.IOException; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CustomAuthEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + + response.setStatus(GeneralErrorCode.UNAUTHORIZED.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + + CustomResponse body = + CustomResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + response.getWriter() + .write(objectMapper.writeValueAsString(body)); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java b/src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java new file mode 100644 index 00000000..bff3e442 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java @@ -0,0 +1,8 @@ +package com.example.RealMatch.global.presentation.advice; + +public class DuplicateUserException extends RuntimeException { + + public DuplicateUserException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java b/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java new file mode 100644 index 00000000..f40ff4fa --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java @@ -0,0 +1,77 @@ +package com.example.RealMatch.global.presentation.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; + +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralErrorCode; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { + + log.warn("[IllegalArgumentException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.BAD_REQUEST.getStatus()) + .body(CustomResponse.onFailure(GeneralErrorCode.BAD_REQUEST, null)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolation(ConstraintViolationException e) { + + log.warn("[ConstraintViolationException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.INVALID_PAGE.getStatus()) + .body(CustomResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity> handleHandlerMethodValidation(HandlerMethodValidationException e) { + + log.warn("[HandlerMethodValidationException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.INVALID_PAGE.getStatus()) + .body(CustomResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); + } + + @ExceptionHandler(SecurityException.class) + public ResponseEntity> handleSecurityException(SecurityException e) { + + log.warn("[SecurityException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.UNAUTHORIZED.getStatus()) + .body(CustomResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null)); + } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFound(ResourceNotFoundException e) { + + log.warn("[ResourceNotFoundException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.NOT_FOUND.getStatus()) + .body(CustomResponse.onFailure(GeneralErrorCode.NOT_FOUND, null)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnexpectedException(Exception e) { + + log.error("[UnexpectedException]", e); + + return ResponseEntity + .status(GeneralErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(CustomResponse.onFailure(GeneralErrorCode.INTERNAL_SERVER_ERROR, null)); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java b/src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java new file mode 100644 index 00000000..c312ca5d --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package com.example.RealMatch.global.presentation.advice; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java new file mode 100644 index 00000000..43c2f3fe --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java @@ -0,0 +1,12 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + + HttpStatus getStatus(); + + String getCode(); + + String getMessage(); +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java new file mode 100644 index 00000000..6ae9b199 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java @@ -0,0 +1,12 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +public interface BaseSuccessCode { + + HttpStatus getStatus(); + + String getCode(); + + String getMessage(); +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java new file mode 100644 index 00000000..a9190d13 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java @@ -0,0 +1,48 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GeneralErrorCode implements BaseErrorCode { + + BAD_REQUEST(HttpStatus.BAD_REQUEST, + "COMMON400_1", + "잘못된 요청입니다."), + + INVALID_DATA(HttpStatus.BAD_REQUEST, + "COMMON4001_2", + "유효하지 않은 데이터입니다."), + + INVALID_PAGE(HttpStatus.BAD_REQUEST, + "COMMON400_3", + "페이지 번호는 1 이상이어야 합니다."), + + DUPLICATE_MISSION(HttpStatus.BAD_REQUEST, + "COMMON400_4", + "이미 도전 중인 미션입니다."), + + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "COMMON401_1", + "인증이 필요합니다."), + + FORBIDDEN(HttpStatus.FORBIDDEN, + "COMMON403_1", + "접근 권한이 없습니다."), + + NOT_FOUND(HttpStatus.NOT_FOUND, + "COMMON404_1", + "요청한 리소스를 찾을 수 없습니다."), + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "COMMON500_1", + "서버 내부 오류가 발생했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java new file mode 100644 index 00000000..b326b6e5 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java @@ -0,0 +1,32 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GeneralSuccessCode implements BaseSuccessCode { + + GOOD_REQUEST(HttpStatus.OK, + "COMMON200_1", + "정상적인 요청입니다."), + AUTHORIZED(HttpStatus.CREATED, + "AUTH201_1", + "인증이 확인되었습니다."), + CREATE(HttpStatus.CREATED, + "CREATE200_1", + "성공적으로 생성되었습니다."), + ALLOWED(HttpStatus.ACCEPTED, + "AUTH203_1", + "요청이 허용되었습니다."), + FOUND(HttpStatus.FOUND, + "COMMON302_1", + "요청한 리소스를 찾았습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/RealMatch/match/application/service/.keep b/src/main/java/com/example/RealMatch/match/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/domain/entity/.keep b/src/main/java/com/example/RealMatch/match/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/.keep b/src/main/java/com/example/RealMatch/match/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/presentation/controller/.keep b/src/main/java/com/example/RealMatch/match/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/match/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/match/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/application/service/.keep b/src/main/java/com/example/RealMatch/user/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/domain/entity/.keep b/src/main/java/com/example/RealMatch/user/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/.keep b/src/main/java/com/example/RealMatch/user/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/presentation/controller/.keep b/src/main/java/com/example/RealMatch/user/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/user/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/user/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/.keep b/src/main/resources/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..074981e3 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,48 @@ +cors: + allowed-origin: http://${SERVER_IP} + +swagger: + server-url: ${SWAGGER_SERVER_URL} + +server: + timezone: Asia/Seoul + +spring: + application: + name: Realmatch + activate: + on-profile: prod + + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME} + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + connection-timeout: 10000 + max-lifetime: 600000 + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + + security: + refresh-token: + expire-days: 14 + +logging: + level: + org.hibernate.SQL: warn + +jwt: + secret: ${JWT_SECRET} + access-expire-ms: ${JWT_ACCESS_EXPIRE_MS} + refresh-expire-ms: ${JWT_REFRESH_EXPIRE_MS} + diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..d2881f5b --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,42 @@ +cors: + allowed-origin: http://localhost:3000 + +swagger: + server-url: http://localhost:8080 + +server: + timezone: Asia/Seoul + +spring: + application: + name: Realmatch + + datasource: + url: jdbc:mysql://localhost:3306/test_db + username: test_user + password: test + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + connection-timeout: 10000 + max-lifetime: 600000 + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show-sql: false + + security: + refresh-token: + expire-days: 14 + +jwt: + secret: ${JWT_SECRET} + access-expire-ms: ${JWT_ACCESS_EXPIRE_MS} + refresh-expire-ms: ${JWT_REFRESH_EXPIRE_MS} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index 5487359f..00000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -spring: - application: - name: RealMatch diff --git a/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java b/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java index 73c5c16a..1228974e 100644 --- a/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java +++ b/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class RealMatchApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/src/test/resources/.keep b/src/test/resources/.keep new file mode 100644 index 00000000..e69de29b