Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
00e4d3a
:construction_worker: chore(ci): container 버전을 위한 github.sha 부분 우선 제거…
Aug 17, 2025
81746dd
:sparkles: feat: User Entity 위한 enum(lang, school, voice, role) 추가 [K…
Aug 18, 2025
e83cac6
:sparkles: feat: User Entity 추가 [KOBG-12]
Aug 18, 2025
073c9da
:sparkles: feat: User Repository 추가 [KOBG-12]
Aug 18, 2025
722fb76
:package: chore(deps): google login, jwt 관련 라이브러리 추가 [KOBG-12]
Aug 18, 2025
b659446
:sparkles: feat: redis 설정 클래스 추가 [KOBG-12]
Aug 18, 2025
1b1a09c
:sparkles: feat: redis에 값 저장, 삭제 등 관련 util 추가 [KOBG-12]
Aug 18, 2025
66f94e4
:sparkles: feat: jwt 토큰 생성, 유효성 검증 및 정보 추츨 등 관련 util 추가 [KOBG-12]
Aug 18, 2025
b827176
:sparkles: feat: access/refresh token 담을 vo 추가 [KOBG-12]
Aug 18, 2025
99b6961
:sparkles: feat: google id token 유효성 검증 관련 util 추가 [KOBG-12]
Aug 18, 2025
bc26909
:sparkles: feat: 필터 예외 처리를 위한 응답 util 추가 [KOBG-12]
Aug 18, 2025
7f7e68b
:sparkles: feat: 로그인 및 회원가입 관련 에러 코드 추가 [KOBG-12]
Aug 18, 2025
ce1b6ab
:sparkles: feat: 필터 예외 처리를 위한 FillterException 추가 [KOBG-12]
Aug 18, 2025
8dbdea9
:sparkles: feat: 필터 예외 처리를 위해 예외 핸들러에 추가 [KOBG-12]
Aug 18, 2025
426b04c
:sparkles: feat: 로그인 및 사용자 간단 정보 응답을 위한 dto 추가 [KOBG-12]
Aug 18, 2025
51fba58
:sparkles: feat: 회원가입 요청을 위한 dto 추가 [KOBG-12]
Aug 18, 2025
e18cfd4
:sparkles: feat: 회원가입 및 사용자 정보 조회 및 수정 로직 구현 [KOBG-12]
Aug 18, 2025
f355d91
:sparkles: feat: 회원가입 및 사용자 정보 조회 및 수정 api 구현 [KOBG-12]
Aug 18, 2025
d3534ba
:memo: docs: 회원가입 및 사용자 정보 조회 및 수정 api 문서 추가 [KOBG-12]
Aug 18, 2025
4e9ff26
:sparkles: feat: jwt 인증 로직을 위한 JwtAuthorizationFilter 추가 [KOBG-12]
Aug 18, 2025
338ee30
:sparkles: feat: JwtAuthorizationFilter 등록 설정 추가 [KOBG-12]
Aug 18, 2025
639d482
:construction_worker: chore(ci): cd 트리거 시점 수정 [KOBG-12]
Aug 18, 2025
c56f51f
:bug: fix: auditing 기능 추가 [KOBG-12]
Aug 18, 2025
7e4f4ea
:wrench: chore(deps): ci pipeline rds 연결 불가로 인한 에러 수정 위해 h2 package 추…
Aug 18, 2025
9ae6edd
:construction_worker: chore(ci): test 용 application.yml 따로 properties…
Aug 18, 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
8 changes: 3 additions & 5 deletions .github/workflows/cd-pipeline.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: Java CD with Gradle

on:
push:
pull_request:
branches:
- main
- feature/KOBG-4/initial-setting
- develop

permissions:
contents: read
Expand Down Expand Up @@ -53,7 +52,6 @@ jobs:
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
script: |
cd /home/ubuntu/app
export IMAGE_TAG=${{ github.sha }} sudo docker compose up -d --pull always
sudo docker compose up -d --pull always
sudo docker ps
sudo docker image prune -f
2 changes: 1 addition & 1 deletion .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: |
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.PROPERTIES }}" > ./application.yml
echo "${{ secrets.PROPERTIES_TEST }}" > ./application.yml
shell: bash

- name: 🚀 Redis 실행
Expand Down
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// google api client
implementation 'com.google.api-client:google-api-client:2.4.0'
implementation 'com.google.http-client:google-http-client-jackson2:1.41.5'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.2'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'

Expand All @@ -47,6 +56,7 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.mockito:mockito-core:5.18.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0'
testImplementation 'com.h2database:h2'
}

compileJava.options.encoding = 'UTF-8'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/edu/kobridge/KobridgeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class KobridgeApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.edu.kobridge.global.common.filter;

import java.io.IOException;

import com.edu.kobridge.global.error.ErrorCode;
import com.edu.kobridge.global.error.GlobalErrorCode;
import com.edu.kobridge.global.error.exception.AppException;
import com.edu.kobridge.global.error.exception.FilterException;
import com.edu.kobridge.global.util.JwtUtil;
import com.edu.kobridge.global.util.ResponseUtil;
import com.edu.kobridge.user.domain.entity.User;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RequiredArgsConstructor
@Slf4j
public class JwtAuthorizationFilter implements Filter {
private final JwtUtil jwtUtil;

// JWT 검사 제외할 경로 설정
final String LOGIN_PATH = "/api/user/google-login";
final String TOKEN_PATH = "/api/user/token";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Filter initialized.");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;

// 요청 URI를 가져옴
String requestURI = req.getRequestURI();

// swagger 인증 필터링 없이 처리
if (requestURI.startsWith("/swagger-ui/") ||
requestURI.startsWith("/webjars/") ||
requestURI.startsWith("/v3/api-docs")) {
chain.doFilter(request, response);
return;
}

// 로그인 및 토큰 재발급 요청은 JWT 인증 필터링 없이 처리
if (requestURI.equals(LOGIN_PATH) && req.getMethod().equals("GET")) {
chain.doFilter(request, response);
return;
} else if (requestURI.equals(TOKEN_PATH) && req.getMethod().equals("GET")) {
chain.doFilter(request, response);
return;
} else if (requestURI.contains("test/no-auth")) {
chain.doFilter(request, response);
return;
}

try {
// Authorization 헤더에서 JWT 토큰을 가져옴
String header = req.getHeader("Authorization");
if (header == null) {
throw new FilterException(GlobalErrorCode.ACCESS_TOKEN_REQUIRED);
}

// 토큰 유효성 검사 후 사용자 정보 추출
User user = jwtUtil.validateToken(true, header);
req.setAttribute("user", user);

// 필터 체인 다음 필터로 이동
chain.doFilter(request, response);
} catch (FilterException e) { // 토큰이 없는 경우
ResponseUtil.setResponse(res, e.getErrorCode().getHttpStatus().value(), e.getMessage());
} catch (AppException e) { // 토큰이 유효하지 않은 경우
ResponseUtil.setResponse(res, e.getErrorCode().getHttpStatus().value(), e.getMessage());
} catch (JwtException e) { // JWT 토큰 예외 처리
ErrorCode code =
e instanceof ExpiredJwtException ? GlobalErrorCode.EXPIRED_JWT : GlobalErrorCode.INVALID_TOKEN;

ResponseUtil.setResponse(res, code.getHttpStatus().value(), code.getMessage());
}
}

@Override
public void destroy() {
log.info("Filter destroyed.");
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/edu/kobridge/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.edu.kobridge.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import lombok.Getter;

@Configuration
@Getter
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;

// 레디스 연결
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration();
redisConfiguration.setHostName(host);
redisConfiguration.setPort(port);

return new LettuceConnectionFactory(redisConfiguration);
}

// 문자열 키 직렬화
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());

return redisTemplate;
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.edu.kobridge.global.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.edu.kobridge.global.common.filter.JwtAuthorizationFilter;
import com.edu.kobridge.global.util.JwtUtil;

import jakarta.servlet.Filter;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final JwtUtil jwtUtil;

// jwt 필터 등록
@Bean
public FilterRegistrationBean<Filter> filterBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(
new JwtAuthorizationFilter(jwtUtil));
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");

return filterRegistrationBean;
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/edu/kobridge/global/enums/JwtVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.edu.kobridge.global.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class JwtVo {
private final String accessToken;
private final String refreshToken;
}
29 changes: 29 additions & 0 deletions src/main/java/com/edu/kobridge/global/enums/LangType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.edu.kobridge.global.enums;

import java.util.Arrays;

import lombok.Getter;

@Getter
public enum LangType {
ENG("ENG", "영어"),
VET("VET", "베트남어"),
CHN("CHN", "중국어"),
JPN("JPN", "일본어"),
NONE("NONE", "none");

private final String code;
private final String name;

LangType(String code, String name) {
this.code = code;
this.name = name;
}

public static UserRoleType of(String code) {
return Arrays.stream(UserRoleType.values())
.filter(r -> r.getCode().equals(code))
.findAny()
.orElse(null);
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/edu/kobridge/global/enums/SchoolType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.edu.kobridge.global.enums;

import java.util.Arrays;

import lombok.Getter;

@Getter
public enum SchoolType {
ELEMENTARY("elementary", "초등학교", 6),
MIDDLE("middle-school", "중학교", 3),
HIGH("high-school", "고등학교", 3);

private final String code;
private final String name;
private final int maxGrade;

SchoolType(String code, String name, int maxGrade) {
this.code = code;
this.name = name;
this.maxGrade = maxGrade;
}

public static SchoolType of(String code) {
return Arrays.stream(SchoolType.values())
.filter(r -> r.getCode().equals(code))
.findAny()
.orElse(null);
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/edu/kobridge/global/enums/UserRoleType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.edu.kobridge.global.enums;

import java.util.Arrays;

import lombok.Getter;

@Getter
public enum UserRoleType {
USER("ROLE_USER", "일반 사용자 권한"),
ADMIN("ROLE_ADMIN", "관리자 권한"),
GUEST("GUEST", "게스트 권한");

private final String code;
private final String name;

UserRoleType(String code, String name) {
this.code = code;
this.name = name;
}

public static UserRoleType of(String code) {
return Arrays.stream(UserRoleType.values())
.filter(r -> r.getCode().equals(code))
.findAny()
.orElse(GUEST);
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/edu/kobridge/global/enums/VoiceType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.edu.kobridge.global.enums;

import java.util.Arrays;

import lombok.Getter;

@Getter
public enum VoiceType {
ONE("GIRL_ONE"),
TWO("GIRL_TWO"),
THREE("BOY_ONE"),
FOUR("BOY_TWO");

// 추후 voice 확정 되면 name 필드 추가
private final String code;

VoiceType(String code) {
this.code = code;
}

public static VoiceType of(String code) {
return Arrays.stream(VoiceType.values())
.filter(r -> r.getCode().equals(code))
.findAny()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

@Getter
public enum GlobalErrorCode implements ErrorCode {
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "인증에 실패하였습니다."),
AUTHORIZATION_FAILED(HttpStatus.UNAUTHORIZED, "인가에 실패하였습니다."),
ACCESS_TOKEN_REQUIRED(HttpStatus.UNAUTHORIZED, "Access Token이 필요합니다."),
REFRESH_TOKEN_REQUIRED(HttpStatus.UNAUTHORIZED, "Refresh Token이 필요합니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Token이 유효하지 않습니다."),
EXPIRED_JWT(HttpStatus.FORBIDDEN, "Token이 만료되었습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 사용자를 찾을 수 없습니다."),
MISSING_HEADER(HttpStatus.BAD_REQUEST, "필수 요청 헤더가 누락되었습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "요청 경로가 지원되지 않습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류가 발생했습니다.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.edu.kobridge.global.error.exception;

import com.edu.kobridge.global.error.ErrorCode;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class FilterException extends RuntimeException {
private ErrorCode errorCode;
private String message;

public FilterException(ErrorCode errorCode) {
this.errorCode = errorCode;
this.message = errorCode.getMessage();
}
}
Loading