Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 }}/spot-backend:prod
${{ secrets.DOCKERHUB_USERNAME }}/spot-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/spot
git pull origin main
docker compose pull
docker compose up -d
16 changes: 8 additions & 8 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ jobs:
--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
Expand All @@ -33,19 +39,13 @@ jobs:
java-version: '21'
distribution: 'temurin'
cache: gradle

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Make application.properties
run: |
mkdir -p ./src/main/resources
echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml
shell: bash

- name: Check code formatting
run: ./gradlew checkstyleMain checkstyleTest
continue-on-error: true
Expand All @@ -66,7 +66,7 @@ jobs:
# 최대 60초 대기하면서 애플리케이션 시작 확인
for i in {1..60}; do
# 로그에서 시작 완료 메시지 확인
if grep -q "Started SpotApplication" bootrun.log 2>/dev/null; then
if grep -q "Started RealMatchApplication" bootrun.log; then
echo "✅ Application started successfully after ${i}s"
kill $APP_PID 2>/dev/null || true
exit 0
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# 특정 파일 제외
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/
build/
Expand Down
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ dependencies {

// mySQL
runtimeOnly 'com.mysql:mysql-connector-j'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'

// postgresql (CI test 용)
runtimeOnly 'org.postgresql:postgresql'
}

tasks.named('test') {
Expand Down
25 changes: 13 additions & 12 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -34,12 +34,13 @@ services:
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}
Original file line number Diff line number Diff line change
@@ -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/v1/test",
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html"
};

private static final String[] REQUEST_AUTHENTICATED_ARRAY = {
"/api/v1/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;
}
}

Original file line number Diff line number Diff line change
@@ -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)
));
}

}
Original file line number Diff line number Diff line change
@@ -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<? extends GrantedAuthority> 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;
}
}
Loading