Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
85b19b7
[Feat] 소셜 로그인 및 토큰 재발급 API 개발 (#24)
soonge2 Apr 30, 2025
1ba733f
[Chore] dev 배포 환경 준비 및 테스트용 기능 추가 (#26)
soonge2 May 1, 2025
1ee33cc
[Feat] 프론트-백-AI 서버 간 연동 테스트를 위한 엔드포인트 추가 (#27)
soonge2 May 1, 2025
e78e1ff
chore: MockAiControllerTest 삭제 및 local 프로필 추가
soonge2 May 1, 2025
376019a
chore: .gitignore에 .env 파일 및 로컬 실행 스크립트(.sh) 추가
soonge2 May 1, 2025
b03b651
[Feat] JWT 인증 필터 구현 (#39)
soonge2 May 1, 2025
1d7ebcc
[Feat] 로그아웃 API 구현 및 인증 실패 시 401 응답 처리 (#40)
soonge2 May 1, 2025
a463912
[Feat] 투표 도메인 엔티티 및 리포지토리 생성 (#41)
soonge2 May 2, 2025
9c35c06
[Feat] 투표 등록 API 개발 (#42)
soonge2 May 3, 2025
be605d1
[Feat] 투표 내용 조회 API 개발 (#43)
soonge2 May 3, 2025
85525e0
[Feat] 투표 참여 API 개발 (#45)
soonge2 May 3, 2025
4855271
[Feat] 투표 결과 조회 API 개발 (#46)
soonge2 May 4, 2025
c785df7
[Feat] Dev 환경에 Jwt 인증 및 예외 처리 필터 적용 (#48)
soonge2 May 4, 2025
c586342
[CICD] Dev 서버용 배포 파이프라인 생성 (#49)
yunabyte May 4, 2025
2b02301
[Feat] 진행 중인 투표 목록 조회 API 개발 (#50)
soonge2 May 5, 2025
c8466ac
[Feat] 내가 만든 투표 목록 조회 API 개발 (#57)
soonge2 May 6, 2025
c33ff95
[Feat] 내가 참여한 투표 목록 조회 API 개발 (#58)
soonge2 May 6, 2025
d9045ad
[Feat] 가입한 그룹 라벨 조회 API 개발 (#59)
soonge2 May 6, 2025
d0aa66e
[Feat] 가입한 그룹 목록 조회 API 개발 (#60)
soonge2 May 6, 2025
4d5c325
[Feat] 그룹 가입 API 개발 (#61)
soonge2 May 6, 2025
b97d9df
[Feat] 새 그룹 생성 API 개발 (#62)
soonge2 May 6, 2025
93a467e
[Feat] 회원정보 조회/수정 API 개발 (#63)
soonge2 May 6, 2025
87764f5
[Feat] 회원 탈퇴 API 개발 (#64)
soonge2 May 7, 2025
591c23d
[CICD] 환경변수 추가 주입 과정 추가
May 7, 2025
d68a20e
[Feat] 랜덤 닉네임 생성 기능 구현 (#65)
soonge2 May 7, 2025
6836a4d
[Chore] Dev CORS에 localhost 추가
soonge2 May 7, 2025
437f9d0
[Fix] 투표 결과 비율 계산에 double 타입 적용
soonge2 May 7, 2025
ae487de
[Feat] 카카오 로그인 시 동적 redirect URI 허용 및 검증 추가 (#67)
soonge2 May 7, 2025
259f6e0
[Refactor] OAuth ID 타입 Long → String으로 변경
soonge2 May 7, 2025
31eec6d
[Feat] 투표 등록 시 익명 기능 추가 (#70)
soonge2 May 8, 2025
3c65947
[Feat] AI 기반 투표 내용 검열 기능 추가 (#71)
soonge2 May 8, 2025
0b4f008
[Fix] USER_NOT_FOUND 에러 상태 코드 변경 (404 → 401)
soonge2 May 8, 2025
22b9b8b
[Fix] 진행 중인 투표 조회 시 PENDING/REJECTED 필터링 추가
soonge2 May 8, 2025
64b3c3b
[Chore] prod 환경의 JPA ddl-auto 옵션을 none → update로 변경
soonge2 May 8, 2025
fd47983
Merge branch 'main' into develop
soonge2 May 8, 2025
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
89 changes: 89 additions & 0 deletions .github/workflows/cicd-bigbang-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: SpringBoot BigBang CI/CD - Dev V1

on:
push:
branches:
- develop

jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: dev

steps:
- name: 🐵 Checkout Repository
uses: actions/checkout@v3

- name: 🐵 Set up JDK 21
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '21'

- name: 🐵 Grant execute permission to Gradle
run: chmod +x gradlew

- name: 🐵 Build with Gradle (Without Tests)
run: |
echo "🐵 Building JAR without tests..."
./gradlew clean build -x test || { echo '🚨 Build failed!'; exit 1; }
echo "✅ Build completed successfully!"

- name: 🐵 Save SSH Key
run: |
echo "🐵 Saving SSH key..."
echo "${{ secrets.GCP_CICD_SSH_KEY }}" > key.pem
chmod 600 key.pem
echo "✅ SSH key saved!"

- name: 🐵 Deploy to GCP VM
run: |
echo "🐵 Sending JAR to GCP..."

scp -i key.pem -o StrictHostKeyChecking=no \
build/libs/moa-server-0.0.1-SNAPSHOT.jar \
cicd@${{ secrets.GCP_BE_DEV_HOST }}:/home/cicd/moa-server-0.0.1-SNAPSHOT.jar || {
echo "🚨 Failed to upload JAR!"; exit 1;
}
echo "✅ JAR uploaded successfully!"

echo "🐵 Writing .env on remote server..."
ssh -i key.pem -o StrictHostKeyChecking=no cicd@${{ secrets.GCP_BE_DEV_HOST }} << 'EOF'
cat <<EENV | sudo tee /home/cicd/moa-backend.env > /dev/null
SPRING_PROFILES_ACTIVE=${{ secrets.SPRING_PROFILE }}
SPRING_DATASOURCE_URL=${{ secrets.DB_URL }}
SPRING_DATASOURCE_USERNAME=${{ secrets.DB_USERNAME }}
SPRING_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
FRONTEND_URL=${{ secrets.FRONTEND_URL }}
AI_SERVER_URL=${{ secrets.AI_SERVER_URL }}
KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}
KAKAO_ADMIN_KEY=${{ secrets.KAKAO_ADMIN_KEY }}
EENV
sudo chmod 600 /home/cicd/moa-backend.env
sudo chown cicd:cicd /home/cicd/moa-backend.env
echo "✅ .env file written and secured!"
EOF

- name: 🐵 Restart systemd Service
run: |
echo "🐵 Restarting Spring Boot systemd service..."
ssh -i key.pem -o StrictHostKeyChecking=no cicd@${{ secrets.GCP_BE_DEV_HOST }} << 'EOF'
echo "🐵 Running daemon-reload and restarting moa-backend..."
sudo systemctl daemon-reload || { echo "🚨 daemon-reload failed!"; exit 1; }
sudo systemctl restart moa-backend || { echo "🚨 Failed to restart service!"; exit 1; }

sleep 7
echo "✅ Service restarted successfully!"

echo "🐵 Checking process..."
if ! sudo pgrep -f 'java -jar' > /dev/null; then
echo "🚨 Spring Boot process not running!"
exit 1
fi

echo "✅ Spring Boot process is running!"

echo "🐵 Tail last 20 lines of server.log:"
sudo tail -n 20 /home/cicd/server.log || echo "🚨 Failed to read server.log"
EOF
12 changes: 3 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
*.env.*
build-local.sh
run-local.sh
37 changes: 36 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,51 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.mysql:mysql-connector-j:8.0.33'
implementation 'com.mysql:mysql-connector-j'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'org.apache.commons:commons-lang3:3.12.0'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// querydsl 의존성
implementation "com.querydsl:querydsl-core:5.1.0"
implementation "com.querydsl:querydsl-jpa:5.1.0:jakarta"

annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

// testImplementation 'org.springframework.boot:spring-boot-starter-test'
// testImplementation 'jakarta.persistence:jakarta.persistence-api'
// testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
}

tasks.named('test') {
useJUnitPlatform()
}

// QueryDSL Q파일 생성용
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

sourceSets {
main {
java {
srcDir querydslDir
}
}
}

tasks.withType(JavaCompile).configureEach {
options.generatedSourceOutputDirectory.set(querydslDir)
}

compileJava {
options.annotationProcessorPath = configurations.annotationProcessor
}
13 changes: 13 additions & 0 deletions db/init-data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- 시스템 유저
INSERT INTO `user` (id, nickname, email, role, user_status, last_active_at, created_at, updated_at)
SELECT 1, 'SYSTEM', 'system@moa.com', 'ADMIN', 'ACTIVE', NOW(), NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM `user` WHERE id = 1
);

-- 공개 그룹
INSERT INTO `group` (id, user_id, name, description, invite_code, created_at, updated_at)
SELECT 1, 1, '공개', '모든 사용자가 속한 공개 투표 그룹입니다.', 'public', NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM `group` WHERE id = 1
);
14 changes: 14 additions & 0 deletions src/main/java/com/moa/moa_server/config/RestTemplateConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moa.moa_server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.moa.moa_server.config.jpa;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.moa.moa_server.config.querydsl;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {

@PersistenceContext
private EntityManager em;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.moa.moa_server.config.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moa.moa_server.domain.global.dto.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

ApiResponse apiResponse = new ApiResponse("INVALID_TOKEN", null);
response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.moa.moa_server.config.security;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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 java.util.List;

import static com.moa.moa_server.config.security.SecurityConstants.ALLOWED_URLS;

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
@Profile("dev")
public class DevSecurityConfig {

@Value("${frontend.url}")
private String frontendUrl;

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.exceptionHandling(ex -> ex.authenticationEntryPoint(customAuthenticationEntryPoint))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/v1/votes").permitAll()
.requestMatchers(ALLOWED_URLS).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

/**
* CORS 정책을 설정하고, 이를 Spring Security에 등록하는 Bean을 반환
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();

config.setAllowedOriginPatterns(List.of(frontendUrl, "http://localhost:5173")); // 요청을 허용할 출처(origin) 패턴을 설정
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); // 허용할 HTTP 메서드 목록 지정
config.setAllowedHeaders(List.of("*")); // 모든 요청 헤더 허용
config.setAllowCredentials(true); // 인증 정보를 포함한 요청(Cookie 등)을 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 경로 별로 다른 CORS 설정을 적용할 수 있도록 지원하는 구현체
source.registerCorsConfiguration("/**", config); // 모든 경로에 위에서 설정한 CORS 정책을 적용
return source;
}

@Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.moa.moa_server.config.security;

import com.moa.moa_server.domain.auth.handler.AuthException;
import com.moa.moa_server.domain.auth.service.JwtTokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Optional;

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenService jwtTokenService; // 토큰 유효성 검증 및 userId 추출

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

try {
// 헤더에서 토큰 추출
Optional<String> tokenOptional = extractToken(request);

if (tokenOptional.isPresent()) {
// 토큰 검증 및 userId 추출
String token = tokenOptional.get();
Long userId = jwtTokenService.validateAndExtractUserId(token);

// 인증 객체 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userId, null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

// SecurityContext에 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
} catch (AuthException e) {
// AuthException을 AuthenticationException으로 감싸서 Spring Security로 위임
throw new AuthenticationCredentialsNotFoundException(e.getCode(), e);
}
}

private Optional<String> extractToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return Optional.of(authHeader.substring(7));
}
return Optional.empty();
}
}
Loading