From 00e4d3a1c652f0f578afba6a93b5bc1af7831337 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 07:07:11 +0900 Subject: [PATCH 01/25] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20co?= =?UTF-8?q?ntainer=20=EB=B2=84=EC=A0=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20gi?= =?UTF-8?q?thub.sha=20=EB=B6=80=EB=B6=84=20=EC=9A=B0=EC=84=A0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 2378d74..22b75c2 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -53,7 +53,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 From 81746dd03970c6d511fafea4ea818d9b22e78f0c Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:29:13 +0900 Subject: [PATCH 02/25] =?UTF-8?q?:sparkles:=20feat:=20User=20Entity=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20enum(lang,=20school,=20voice,=20role)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/global/enums/LangType.java | 29 +++++++++++++++++++ .../edu/kobridge/global/enums/SchoolType.java | 29 +++++++++++++++++++ .../kobridge/global/enums/UserRoleType.java | 27 +++++++++++++++++ .../edu/kobridge/global/enums/VoiceType.java | 27 +++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/enums/LangType.java create mode 100644 src/main/java/com/edu/kobridge/global/enums/SchoolType.java create mode 100644 src/main/java/com/edu/kobridge/global/enums/UserRoleType.java create mode 100644 src/main/java/com/edu/kobridge/global/enums/VoiceType.java diff --git a/src/main/java/com/edu/kobridge/global/enums/LangType.java b/src/main/java/com/edu/kobridge/global/enums/LangType.java new file mode 100644 index 0000000..8d34f10 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/LangType.java @@ -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); + } +} diff --git a/src/main/java/com/edu/kobridge/global/enums/SchoolType.java b/src/main/java/com/edu/kobridge/global/enums/SchoolType.java new file mode 100644 index 0000000..4bb703f --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/SchoolType.java @@ -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); + } +} diff --git a/src/main/java/com/edu/kobridge/global/enums/UserRoleType.java b/src/main/java/com/edu/kobridge/global/enums/UserRoleType.java new file mode 100644 index 0000000..3557475 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/UserRoleType.java @@ -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); + } +} diff --git a/src/main/java/com/edu/kobridge/global/enums/VoiceType.java b/src/main/java/com/edu/kobridge/global/enums/VoiceType.java new file mode 100644 index 0000000..fed6ac7 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/VoiceType.java @@ -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); + } +} From e83cac697d93080d9eee2a898875437e62abfc39 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:30:28 +0900 Subject: [PATCH 03/25] =?UTF-8?q?:sparkles:=20feat:=20User=20Entity=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/user/domain/entity/User.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/domain/entity/User.java diff --git a/src/main/java/com/edu/kobridge/user/domain/entity/User.java b/src/main/java/com/edu/kobridge/user/domain/entity/User.java new file mode 100644 index 0000000..8f594ba --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/domain/entity/User.java @@ -0,0 +1,82 @@ +package com.edu.kobridge.user.domain.entity; + +import com.edu.kobridge.global.common.BaseTime; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.SchoolType; +import com.edu.kobridge.global.enums.UserRoleType; +import com.edu.kobridge.global.enums.VoiceType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class User extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(unique = true) + private String email; + + private String name; + + @Enumerated(EnumType.STRING) + private LangType lang; + + @Enumerated(EnumType.STRING) + private SchoolType school; + + private byte grade; + + @Enumerated(EnumType.STRING) + private VoiceType voice; + + private int level; + + @NotNull + @Enumerated(EnumType.STRING) + private UserRoleType role; + + public static User of(@NotNull String email) { + return User + .builder() + .email(email) + .role(UserRoleType.USER) + .build(); + } + + public void updateInfo(@NotNull String name, @NotNull LangType lang, @NotNull SchoolType school, + @NotNull byte grade, @NotNull VoiceType voice) { + this.name = name; + this.lang = lang; + this.school = school; + this.grade = grade; + this.voice = voice; + this.level = 1; + } + + public void updateLang(@NotNull LangType lang) { + this.lang = lang; + } + + public void updateLevel(@NotNull int level) { + this.level = level; + } +} From 073c9daa1ef0842541bfc2505637c9a729c0b48f Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:31:28 +0900 Subject: [PATCH 04/25] =?UTF-8?q?:sparkles:=20feat:=20User=20Repository=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/domain/repository/UserRepository.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java diff --git a/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java b/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java new file mode 100644 index 0000000..c03bda2 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.edu.kobridge.user.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.edu.kobridge.user.domain.entity.User; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} From 722fb76a832aa48341638e489eeabbed4b8b9776 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:33:06 +0900 Subject: [PATCH 05/25] =?UTF-8?q?:package:=20chore(deps):=20google=20login?= =?UTF-8?q?,=20jwt=20=EA=B4=80=EB=A0=A8=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.gradle b/build.gradle index 7d835eb..0ac4ea0 100644 --- a/build.gradle +++ b/build.gradle @@ -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' From b6594466620f150303ad2a19db0d63045fb956c4 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:25:31 +0900 Subject: [PATCH 06/25] =?UTF-8?q?:sparkles:=20feat:=20redis=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/config/RedisConfig.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/config/RedisConfig.java diff --git a/src/main/java/com/edu/kobridge/global/config/RedisConfig.java b/src/main/java/com/edu/kobridge/global/config/RedisConfig.java new file mode 100644 index 0000000..a700134 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/config/RedisConfig.java @@ -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 redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} From 1b1a09cc9184970ef9454e5c60dcd851b22603f9 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:25:52 +0900 Subject: [PATCH 07/25] =?UTF-8?q?:sparkles:=20feat:=20redis=EC=97=90=20?= =?UTF-8?q?=EA=B0=92=20=EC=A0=80=EC=9E=A5,=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=93=B1=20=EA=B4=80=EB=A0=A8=20util=20=EC=B6=94=EA=B0=80=20[K?= =?UTF-8?q?OBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/global/util/RedisUtil.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/RedisUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/RedisUtil.java b/src/main/java/com/edu/kobridge/global/util/RedisUtil.java new file mode 100644 index 0000000..6228dcf --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/RedisUtil.java @@ -0,0 +1,38 @@ +package com.edu.kobridge.global.util; + +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RedisUtil { + private final RedisTemplate redisTemplate; + + // 레디스에 유효 시간에 맞춰 값 저장 + public void setOpsForValue(String key, String value, int expire_h) { + ValueOperations stringValueOperations = redisTemplate.opsForValue(); + stringValueOperations.set(key, value); + redisTemplate.expire(key, expire_h, TimeUnit.HOURS); + + log.info("[Redis] Data saved successfully. -- " + key); + } + + // 레디스에 저장된 값 가져오기 + public String getOpsForValue(String key) { + return (String)redisTemplate.opsForValue().get(key); + } + + // 레디스에 저장된 값 삭제 + public void delete(String key) { + redisTemplate.delete(key); + + log.info("[Redis] Data deleted successfully. -- " + key); + } +} From 66f94e44508e3bfda711f2174b1408665fca88e7 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:29:11 +0900 Subject: [PATCH 08/25] =?UTF-8?q?:sparkles:=20feat:=20jwt=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1,=20=EC=9C=A0=ED=9A=A8=EC=84=B1?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EC=B8=A8=20=EB=93=B1=20=EA=B4=80=EB=A0=A8=20util=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/edu/kobridge/global/util/JwtUtil.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/JwtUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/JwtUtil.java b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java new file mode 100644 index 0000000..7d94507 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java @@ -0,0 +1,125 @@ +package com.edu.kobridge.global.util; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.edu.kobridge.global.enums.JwtVo; +import com.edu.kobridge.global.error.GlobalErrorCode; +import com.edu.kobridge.global.error.exception.AppException; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.domain.repository.UserRepository; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtUtil { + @Value("${jwt.issuer}") + private String ISSUER; + + @Value("${jwt.secret}") + private String JWT_SECRET_KEY; + + @Value("${jwt.access-token-expiration}") + private int ACCESS_TOKEN_EXPIRATION; + + @Getter + @Value("${jwt.refresh-token-expiration}") + private int REFRESH_TOKEN_EXPIRATION; + + private final String PAYLOAD_KEY_ID = "id"; + + private final UserRepository userRepository; + private final RedisUtil redisUtil; + + public JwtVo generateTokens(User user) { + final String PAYLOAD_KEY_EMAIL = "email"; + + // payload에 사용자 식별 값 추가 + Map payloads = new LinkedHashMap<>(); + payloads.put(PAYLOAD_KEY_ID, user.getId()); + payloads.put(PAYLOAD_KEY_EMAIL, user.getEmail()); + + // Token 유효기간 설정 + Date now = new Date(); + Date accessExp = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION); + Date refreshExp = new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION); + + // 토큰 생성 + return new JwtVo( + createToken(payloads, accessExp, "access"), + createToken(payloads, refreshExp, "refresh") + ); + } + + // JWT 생성 메서드 + private String createToken(Map payloads, Date expiration, String subject) { + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(payloads) + .setIssuer(ISSUER) + .setIssuedAt(new Date()) + .setExpiration(expiration) + .setSubject(subject) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY.getBytes()) + .compact(); + } + + // 토큰을 검증하고 유효한 사용자 정보를 반환 + @Transactional(readOnly = true) + public User validateToken(boolean isAccessToken, String header) throws AppException { + // Jwt 토큰 및 claims 추출 + String token = extractToken(header); + Claims claims = parseToken(token); + + // 사용자 정보 추출 및 매핑 + Long userId = claims.get(PAYLOAD_KEY_ID, Long.class); + User user = userRepository.findById(userId) + .orElseThrow(() -> new AppException(GlobalErrorCode.USER_NOT_FOUND)); + + if (!isAccessToken) { + // refreshToken 있는지 확인 + String storedRefreshToken = redisUtil.getOpsForValue(user.getId() + "_refresh"); + if (storedRefreshToken == null || !storedRefreshToken.equals(token)) { + throw new AppException(GlobalErrorCode.AUTHORIZATION_FAILED); + } + } + + return user; + } + + // Authorization 헤더에서 Bearer 토큰 추출 + private String extractToken(String header) { + if (header == null || !header.startsWith("Bearer ")) { + throw new AppException(GlobalErrorCode.INVALID_TOKEN); + } + return header.substring(7); + } + + // JWT 토큰에서 클레임(Claims) 추출 + private Claims parseToken(String token) { + try { + return Jwts.parser() + .setSigningKey(JWT_SECRET_KEY.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (JwtException e) { + throw new AppException(GlobalErrorCode.INVALID_TOKEN); + } + } + +} From b8271763dbd44c5588db8dcc1fc4a19355f9a6c3 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:30:05 +0900 Subject: [PATCH 09/25] =?UTF-8?q?:sparkles:=20feat:=20access/refresh=20tok?= =?UTF-8?q?en=20=EB=8B=B4=EC=9D=84=20vo=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/edu/kobridge/global/enums/JwtVo.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/enums/JwtVo.java diff --git a/src/main/java/com/edu/kobridge/global/enums/JwtVo.java b/src/main/java/com/edu/kobridge/global/enums/JwtVo.java new file mode 100644 index 0000000..350b970 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/JwtVo.java @@ -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; +} \ No newline at end of file From 99b69618eac435dd5feeb78b7aadcbb135e94917 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:31:58 +0900 Subject: [PATCH 10/25] =?UTF-8?q?:sparkles:=20feat:=20google=20id=20token?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20util=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/util/GoogleOAuthUtil.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java new file mode 100644 index 0000000..17c7029 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java @@ -0,0 +1,49 @@ +package com.edu.kobridge.global.util; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Collections; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.edu.kobridge.global.error.exception.AppException; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.error.UserErrorCode; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.gson.GsonFactory; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class GoogleOAuthUtil { + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String CLIENT_ID; + + public User authenticate(String idToken) + throws GeneralSecurityException, IOException { + // idToken 유효성 확인을 위한 Google 인증 설정 + HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport(); + GsonFactory gsonFactory = GsonFactory.getDefaultInstance(); + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, gsonFactory) + .setAudience(Collections.singletonList(CLIENT_ID)) + .build(); + + // 전달된 idToken을 검증하여 GoogleIdToken 객체 생성 + GoogleIdToken googleIdToken = verifier.verify(idToken); + if (googleIdToken == null) { + throw new AppException(UserErrorCode.INVALID_ID_TOKEN); + } + + // 검증된 ID 토큰의 페이로드에서 사용자 정보 추출 + GoogleIdToken.Payload payload = googleIdToken.getPayload(); + String email = payload.getEmail(); + + return User.of(email); + } +} From bc26909d6de5b7fbb918144bb7cf5ee4b8112bf5 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:33:41 +0900 Subject: [PATCH 11/25] =?UTF-8?q?:sparkles:=20feat:=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=9D=91=EB=8B=B5=20util=20=EC=B6=94=EA=B0=80=20[K?= =?UTF-8?q?OBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/util/ResponseUtil.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/ResponseUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/ResponseUtil.java b/src/main/java/com/edu/kobridge/global/util/ResponseUtil.java new file mode 100644 index 0000000..690f677 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/ResponseUtil.java @@ -0,0 +1,39 @@ +package com.edu.kobridge.global.util; + +import java.io.IOException; + +import org.springframework.stereotype.Component; + +import com.edu.kobridge.global.common.DataResponseDto; +import com.edu.kobridge.global.common.ResponseDto; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ResponseUtil { + + // 단순 메시지 응답을 설정 + public static void setResponse(HttpServletResponse response, int status, String message) throws IOException { + ResponseDto dto = ResponseDto.of(status, message); + + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.setStatus(status); + response.getWriter().write(new ObjectMapper().writeValueAsString(dto)); + } + + // 데이터가 포함된 응답을 설정 + public static void setDataResponse(HttpServletResponse response, int status, Object data) throws IOException { + ResponseDto dto = DataResponseDto.of(data, status); + + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.setStatus(status); + response.getWriter().write(new ObjectMapper().writeValueAsString(dto)); + } +} From 7f7e68b04be2109790f7bd320fb4e95136967524 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:35:52 +0900 Subject: [PATCH 12/25] =?UTF-8?q?:sparkles:=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/GlobalErrorCode.java | 7 ++++++ .../kobridge/user/error/UserErrorCode.java | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/error/UserErrorCode.java diff --git a/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java b/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java index 1cf72eb..8db06b5 100644 --- a/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java +++ b/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java @@ -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, "내부 서버 오류가 발생했습니다."); diff --git a/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java b/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java new file mode 100644 index 0000000..12b37e2 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java @@ -0,0 +1,22 @@ +package com.edu.kobridge.user.error; + +import org.springframework.http.HttpStatus; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.Getter; + +@Getter +public enum UserErrorCode implements ErrorCode { + ID_TOKEN_REQUIRED(HttpStatus.BAD_REQUEST, "Id Token이 필요합니다."), + INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "유효한 Id Token이 아닙니다."), + GRADE_SIZE_ERROR(HttpStatus.BAD_REQUEST, "중학생, 고등학생은 3학년까지만 존재합니다."); + + private final HttpStatus httpStatus; + private final String message; + + UserErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } +} From ce1b6abee3dbe643de7cdc0b9ff0cef553261dd2 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:37:09 +0900 Subject: [PATCH 13/25] =?UTF-8?q?:sparkles:=20feat:=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20FillterException=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/exception/FilterException.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/error/exception/FilterException.java diff --git a/src/main/java/com/edu/kobridge/global/error/exception/FilterException.java b/src/main/java/com/edu/kobridge/global/error/exception/FilterException.java new file mode 100644 index 0000000..7168ab6 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/error/exception/FilterException.java @@ -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(); + } +} From 8dbdea955e2db9ec9cbd9770eee9c8ce2f0425d1 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:37:43 +0900 Subject: [PATCH 14/25] =?UTF-8?q?:sparkles:=20feat:=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/handler/GlobalExceptionHandler.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java index 86c9372..d4aea85 100644 --- a/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java @@ -15,6 +15,7 @@ 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 lombok.extern.slf4j.Slf4j; @@ -35,6 +36,18 @@ public ResponseEntity exceptionHandler(AppException e) { return ResponseEntity.status(code).body(ResponseDto.of(code, message)); } + // FilterException + @ExceptionHandler(FilterException.class) + public ResponseEntity exceptionHandler(FilterException e) { + int code = e.getErrorCode().getHttpStatus().value(); + String message = e.getMessage(); + + if (code == 500) + e.printStackTrace(); + + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + // HttpMessageNotReadableException @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { From 426b04ce89e6e863cae8c347ee41d6510959e2ed Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:40:17 +0900 Subject: [PATCH 15/25] =?UTF-8?q?:sparkles:=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B0=84?= =?UTF-8?q?=EB=8B=A8=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/user/dto/res/LoginResDto.java | 27 ++++++++++++++ .../edu/kobridge/user/dto/res/UserResDto.java | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java create mode 100644 src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java diff --git a/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java b/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java new file mode 100644 index 0000000..e059245 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java @@ -0,0 +1,27 @@ +package com.edu.kobridge.user.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LoginResDto { + @Schema(description = "accessToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") + private final String accessToken; + + @Schema(description = "refreshToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") + private final String refreshToken; + + @Schema(description = "isFirstLogin : 사용자가 회원가입한 적이 없다면 false 반환 / 사용자가 회원가입한 적이 있다면 true 반환", example = "false") + private final boolean isFirstLogin; + + public static LoginResDto of(String accessToken, String refreshToken, boolean isFirstLogin) { + return LoginResDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .isFirstLogin(isFirstLogin) + .build(); + } +} diff --git a/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java b/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java new file mode 100644 index 0000000..ce76ee0 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java @@ -0,0 +1,36 @@ +package com.edu.kobridge.user.dto.res; + +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.VoiceType; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Schema(description = "사용자 간단 정보 DTO") +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class UserResDto { + + @Schema(description = "이름", example = "김철수") + private final String name; + + @Schema(description = "번역 언어", example = "ENG") + private final LangType lang; + + @Schema(description = "선택한 Voice", example = "ONE") + private final VoiceType voice; + + @Schema(description = "현재 level", example = "3") + private final int level; + + public static UserResDto of(String name, LangType lang, VoiceType voice, int level) { + return UserResDto.builder() + .name(name) + .lang(lang) + .voice(voice) + .level(level) + .build(); + } +} From 51fba5881df7e3d6de460edaa1d28d4e48f290a1 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:40:40 +0900 Subject: [PATCH 16/25] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/user/dto/req/SignUpReqDto.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java diff --git a/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java b/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java new file mode 100644 index 0000000..8569fa4 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java @@ -0,0 +1,43 @@ +package com.edu.kobridge.user.dto.req; + +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.SchoolType; +import com.edu.kobridge.global.enums.VoiceType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(description = "사용자 가입 요청 DTO") +public record SignUpReqDto( + @Schema(description = "이름", example = "김철수") + @NotBlank(message = "이름은 필수 값입니다.") + @Size(min = 2, max = 50, message = "이름은 2자이상 50자 미만이어야 합니다.") + String name, + + @Schema(description = "나이", example = "23") + @Min(value = 1, message = "나이는 1 이상이어야 합니다.") + @Max(value = 100, message = "나이는 100 이하여야 합니다.") + int age, + + @Schema(description = "언어", example = "ENG , VET, CHN, JPN, NONE") + @NotNull(message = "역할은 필수 값입니다.") + LangType lang, + + @Schema(description = "학교", example = "ELEMENTARY , MIDDLE, HIGH") + @NotNull(message = "학교는 필수 값입니다.") + SchoolType school, + + @Schema(description = "학년", example = "3") + @Min(value = 1, message = "학년은 1 이상이어야 합니다.") + @Max(value = 6, message = "학년은 6 이하여야 합니다.") + byte grade, + + @Schema(description = "음성", example = "BASIC, CUSTOM") + @NotNull(message = "음성은 필수 값입니다.") + VoiceType voice +) { +} From e18cfd4e6cb281a94fd7ea392742530fd9e63a48 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:41:24 +0900 Subject: [PATCH 17/25] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/user/service/UserService.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/service/UserService.java diff --git a/src/main/java/com/edu/kobridge/user/service/UserService.java b/src/main/java/com/edu/kobridge/user/service/UserService.java new file mode 100644 index 0000000..9ff5e85 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/service/UserService.java @@ -0,0 +1,127 @@ +package com.edu.kobridge.user.service; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.edu.kobridge.global.enums.JwtVo; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.SchoolType; +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.util.GoogleOAuthUtil; +import com.edu.kobridge.global.util.JwtUtil; +import com.edu.kobridge.global.util.RedisUtil; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.domain.repository.UserRepository; +import com.edu.kobridge.user.dto.req.SignUpReqDto; +import com.edu.kobridge.user.dto.res.LoginResDto; +import com.edu.kobridge.user.dto.res.UserResDto; +import com.edu.kobridge.user.error.UserErrorCode; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Slf4j +public class UserService { + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final GoogleOAuthUtil googleOAuthUtil; + private final UserRepository userRepository; + + @Transactional + public LoginResDto getGoogleLogin(String idToken) { + // 요청 인자 유효성 검사 + if (idToken.isBlank()) { + throw new AppException(UserErrorCode.ID_TOKEN_REQUIRED); + } + + // 구글 인증 진행 + User googleUser = null; + try { + googleUser = googleOAuthUtil.authenticate(idToken); + } catch (GeneralSecurityException | IOException e) { + throw new AppException(UserErrorCode.INVALID_ID_TOKEN); + } + + // 유저 찾고, 없다면 새 유저 생성 + User finalGoogleUser = googleUser; + User user = userRepository.findByEmail(googleUser.getEmail()) + .orElseGet(() -> userRepository.save(finalGoogleUser)); + + // JWT 토큰 생성 및 refreshToken 저장 + JwtVo jwtVo = jwtUtil.generateTokens(user); + redisUtil.setOpsForValue(user.getId() + "_refresh", jwtVo.getRefreshToken(), + jwtUtil.getREFRESH_TOKEN_EXPIRATION()); + + return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken(), user.getLang() == null); + } + + @Transactional + public LoginResDto getAccessToken(String refreshToken) { + if (refreshToken.isBlank()) { + throw new AppException(GlobalErrorCode.REFRESH_TOKEN_REQUIRED); + } + + // refreshToken 유효성 검사 실행 + User tokenUser; + try { + tokenUser = jwtUtil.validateToken(false, refreshToken); + } catch (JwtException e) { + ErrorCode code = + e instanceof ExpiredJwtException ? GlobalErrorCode.EXPIRED_JWT : GlobalErrorCode.INVALID_TOKEN; + + throw new AppException(code); + } + + // JWT 토큰 생성 및 refreshToken 저장 + JwtVo jwtVo = jwtUtil.generateTokens(tokenUser); + redisUtil.setOpsForValue(tokenUser.getId() + "_refresh", jwtVo.getRefreshToken(), + jwtUtil.getREFRESH_TOKEN_EXPIRATION()); + + return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken(), tokenUser.getLang() == null); + } + + @Transactional + public void postSignUp(User user, SignUpReqDto signUpReq) { + // 학년 유효성 검사 + if (signUpReq.school() == SchoolType.MIDDLE || signUpReq.school() == SchoolType.HIGH || signUpReq.grade() > 3) { + throw new AppException(UserErrorCode.GRADE_SIZE_ERROR); + } + + // 사용자 정보 업데이트 + user.updateInfo(signUpReq.name(), signUpReq.lang(), signUpReq.school(), signUpReq.grade(), signUpReq.voice()); + + userRepository.save(user); + } + + public UserResDto getUserInfo(User user) { + return UserResDto.of(user.getName(), user.getLang(), user.getVoice(), user.getLevel()); + } + + @Transactional + public void updateLang(User user, LangType lang) { + user.updateLang(lang); + userRepository.save(user); + } + + @Transactional + public void updateLevel(User user) { + user.updateLevel(user.getLevel() + 1); + userRepository.save(user); + } + + @Transactional + public void deleteGoogleLogout(User user) { + // 사용자 refreshToken 삭제 + redisUtil.delete(user.getId() + "_refresh"); + } +} From f355d91a5423c21b617f25d94692c84018cd153a Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:41:40 +0900 Subject: [PATCH 18/25] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20api=20=EA=B5=AC=ED=98=84=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/controller/UserController.java diff --git a/src/main/java/com/edu/kobridge/user/controller/UserController.java b/src/main/java/com/edu/kobridge/user/controller/UserController.java new file mode 100644 index 0000000..7cc6348 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/controller/UserController.java @@ -0,0 +1,75 @@ +package com.edu.kobridge.user.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.edu.kobridge.global.common.DataResponseDto; +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.dto.req.SignUpReqDto; +import com.edu.kobridge.user.dto.res.LoginResDto; +import com.edu.kobridge.user.service.UserService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/api/user") +@RequiredArgsConstructor +@Slf4j +public class UserController implements UserControllerDocs { + private final UserService userService; + + @GetMapping("/google-login") + public ResponseEntity getGoogleLogin(@RequestHeader("id-token") String idToken) { + LoginResDto resDto = userService.getGoogleLogin(idToken); + return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); + } + + @GetMapping("/token") + public ResponseEntity getAccessToken(@RequestHeader("Authorization-Refresh") String refreshToken) { + LoginResDto resDto = userService.getAccessToken(refreshToken); + return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); + } + + @PostMapping + public ResponseEntity postSignUp(@RequestAttribute("user") User user, + @RequestBody @Valid SignUpReqDto signUpReq) { + userService.postSignUp(user, signUpReq); + return ResponseEntity.ok(ResponseDto.of(200)); + } + + @GetMapping + public ResponseEntity getUserInfo(@RequestAttribute("user") User user) { + return ResponseEntity.status(200).body(DataResponseDto.of(userService.getUserInfo(user), 200)); + } + + @PatchMapping("/lang") + public ResponseEntity patchLang(@RequestAttribute("user") User user, LangType lang) { + userService.updateLang(user, lang); + return ResponseEntity.ok(ResponseDto.of(200)); + } + + @PatchMapping("level") + public ResponseEntity patchLevel(@RequestAttribute("user") User user) { + userService.updateLevel(user); + return ResponseEntity.ok(ResponseDto.of(200)); + } + + @DeleteMapping("/google-logout") + public ResponseEntity deleteGoogleLogout(@RequestAttribute("user") User user) { + userService.deleteGoogleLogout(user); + return ResponseEntity.ok(ResponseDto.of(200)); + } + +} From d3534ba3badae10528a13780f5bc1dda81c043af Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:42:05 +0900 Subject: [PATCH 19/25] =?UTF-8?q?:memo:=20docs:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20api=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerDocs.java | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java diff --git a/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java b/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java new file mode 100644 index 0000000..a96ff63 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java @@ -0,0 +1,269 @@ +package com.edu.kobridge.user.controller; + +import org.springframework.http.ResponseEntity; + +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.dto.req.SignUpReqDto; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +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 = "User", description = "사용자 관련 API") +public interface UserControllerDocs { + + @Operation(summary = "로그인", description = "구글 로그인을 진행합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 201, \"message\": \"Created\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"Id Token이 필요합니다.\" }," + ) + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }," + + "{ \"code\": 404, \"message\": \"해당하는 사용자를 찾을 수 없습니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") + ) + ), + @ApiResponse(responseCode = "404", description = "해당 자원을 찾을 수 없습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 404, \"message\": \"해당하는 사용자를 찾을 수 없습니다.\" }") + ) + ) + }) + ResponseEntity getGoogleLogin(String idToken); + + @Operation(summary = "accessToken 발급", description = "리프레시 토큰을 이용해 엑세스 토큰을 발급합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 201, \"message\": \"Created\" }") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Refresh Token이 필요합니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") + ) + ) + }) + ResponseEntity getAccessToken(String refreshToken); + + @Operation(summary = "회원가입", description = "사용자 회원가입을 진행합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 400, \"message\": \"이름은 필수 값입니다.\" }, " + + "{ \"code\": 400, \"message\": \"이름은 2자이상 50자 미만이어야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"나이는 1 이상이어야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"나이는 100 이하여야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"역할은 필수 값입니다.\" }," + + "{ \"code\": 400, \"message\": \"학교는 필수 값입니다.\" }," + + "{ \"code\": 400, \"message\": \"학년은 1 이상이어야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"학년은 6 이하여야 합니다.\" }" + + "{ \"code\": 400, \"message\": \"중학생, 고등학생은 3학년까지만 존재합니다.\" }" + + "{ \"code\": 400, \"message\": \"음성은 필수 값입니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") + ) + ) + }) + ResponseEntity postSignUp(User user, SignUpReqDto signUpReq); + + @Operation(summary = "사용자 정보 확인", description = "사용자의 이름과 번역 언어, 레벨을 확인합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": {\n" + + " \"name\": \"김철수\",\n" + + " \"lang\": \"ENG\",\n" + + " \"level\": 1\n" + + " }\n" + + "}") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity getUserInfo(User user); + + @Operation(summary = "번역 언어 변경", description = "사용하는 번역 언어를 변경합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"LANG_TYPE 의 값이 아닙니다.\" }" + ) + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity patchLang(User user, LangType lang); + + @Operation(summary = "레벨 완료", description = "레벨을 완료하여 1단게 높아집니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity patchLevel(User user); + + @Operation(summary = "로그아웃", description = "로그아웃을 진행합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity deleteGoogleLogout(User user); + +} + From 4e9ff26202a644ca60875be2435b995a2f4be280 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:43:01 +0900 Subject: [PATCH 20/25] =?UTF-8?q?:sparkles:=20feat:=20jwt=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?JwtAuthorizationFilter=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/filter/JwtAuthorizationFilter.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java diff --git a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java new file mode 100644 index 0000000..477044e --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java @@ -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."); + } +} From 338ee30c1779d05f6e28f95ccc4e4fe2ffda35a1 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:44:17 +0900 Subject: [PATCH 21/25] =?UTF-8?q?:sparkles:=20feat:=20JwtAuthorizationFilt?= =?UTF-8?q?er=20=EB=93=B1=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/config/WebMvcConfig.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java diff --git a/src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java b/src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java new file mode 100644 index 0000000..39e3bc0 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java @@ -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 filterBean() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new JwtAuthorizationFilter(jwtUtil)); + filterRegistrationBean.setOrder(1); + filterRegistrationBean.addUrlPatterns("/*"); + + return filterRegistrationBean; + } +} From 639d482a479608a2eb12cd11a9be0d03943e6d24 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:50:26 +0900 Subject: [PATCH 22/25] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20cd?= =?UTF-8?q?=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=8B=9C=EC=A0=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 22b75c2..4fd3ef1 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -1,10 +1,9 @@ name: Java CD with Gradle on: - push: + pull_request: branches: - - main - - feature/KOBG-4/initial-setting + - develop permissions: contents: read From c56f51f7893dec3611c66da01b6f0ce0372c08c5 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 00:07:09 +0900 Subject: [PATCH 23/25] =?UTF-8?q?:bug:=20fix:=20auditing=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/edu/kobridge/KobridgeApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/edu/kobridge/KobridgeApplication.java b/src/main/java/com/edu/kobridge/KobridgeApplication.java index cddcf21..3846808 100644 --- a/src/main/java/com/edu/kobridge/KobridgeApplication.java +++ b/src/main/java/com/edu/kobridge/KobridgeApplication.java @@ -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 { From 7e4f4eaecfe0a258e92b1786034ad01eedee77b6 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 00:25:33 +0900 Subject: [PATCH 24/25] =?UTF-8?q?:wrench:=20chore(deps):=20ci=20pipeline?= =?UTF-8?q?=20rds=20=EC=97=B0=EA=B2=B0=20=EB=B6=88=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20h2=20package=20=EC=B6=94=EA=B0=80=20[KOBG-?= =?UTF-8?q?12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 0ac4ea0..f680a92 100644 --- a/build.gradle +++ b/build.gradle @@ -56,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' From 9ae6eddc4e334b1218e20e9333cbd81bc1e1fd13 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 00:26:07 +0900 Subject: [PATCH 25/25] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20te?= =?UTF-8?q?st=20=EC=9A=A9=20application.yml=20=EB=94=B0=EB=A1=9C=20propert?= =?UTF-8?q?ies=20=EC=84=A4=EC=A0=95=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 17405f8..3fb3794 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -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 실행