diff --git a/.gitignore b/.gitignore
index c46dead..df991c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,7 +86,6 @@ fabric.properties
.idea/*
-!.idea/codeStyles
!.idea/runConfigurations
### Java ###
@@ -175,4 +174,7 @@ gradle-app.setting
# Java heap dump
*.hprof
-# End of https://www.toptal.com/developers/gitignore/api/java,gradle,macos,intellij+all
\ No newline at end of file
+# End of https://www.toptal.com/developers/gitignore/api/java,gradle,macos,intellij+allauth.yml
+oauth.yml
+database.yml
+jwt.yml
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..9a295ca
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index d391089..3827b7a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,40 +1,47 @@
plugins {
- id 'java'
- id 'org.springframework.boot' version '3.3.2'
- id 'io.spring.dependency-management' version '1.1.6'
+ id 'java'
+ id 'org.springframework.boot' version '3.3.2'
+ id 'io.spring.dependency-management' version '1.1.6'
}
group = 'kaboo'
version = '0.0.1-SNAPSHOT'
java {
- toolchain {
- languageVersion = JavaLanguageVersion.of(17)
- }
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
}
configurations {
- compileOnly {
- extendsFrom annotationProcessor
- }
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
}
repositories {
- mavenCentral()
+ mavenCentral()
}
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'
- compileOnly 'org.projectlombok:lombok'
- runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
- 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'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ compileOnly 'org.projectlombok:lombok'
+ runtimeOnly 'com.h2database:h2'
+ 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'
+
+ // JWT Token Dependency
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
tasks.named('test') {
- useJUnitPlatform()
+ useJUnitPlatform()
}
diff --git a/src/main/java/kaboo/kaboo_auth/config/PasswordEncoderConfig.java b/src/main/java/kaboo/kaboo_auth/config/PasswordEncoderConfig.java
new file mode 100644
index 0000000..7f6c502
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/config/PasswordEncoderConfig.java
@@ -0,0 +1,14 @@
+package kaboo.kaboo_auth.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class PasswordEncoderConfig {
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/config/SecurityConfig.java b/src/main/java/kaboo/kaboo_auth/config/SecurityConfig.java
new file mode 100644
index 0000000..6202af5
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/config/SecurityConfig.java
@@ -0,0 +1,63 @@
+package kaboo.kaboo_auth.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import kaboo.kaboo_auth.domain.handler.LoginSuccessHandler;
+import kaboo.kaboo_auth.domain.jwt.filter.JwtFilter;
+import kaboo.kaboo_auth.service.CustomOAuth2Service;
+import lombok.RequiredArgsConstructor;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtFilter jwtFilter;
+ private final CustomOAuth2Service customOAuth2Service;
+ private final LoginSuccessHandler loginSuccessHandler;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/").permitAll()
+ .anyRequest().authenticated()) // 그 외 요청은 인증 필요
+ .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
+
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .formLogin(AbstractHttpConfigurer::disable)
+ .httpBasic(AbstractHttpConfigurer::disable)
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+ http.oauth2Login(auth -> auth
+ .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2Service))
+ .successHandler(loginSuccessHandler));
+
+ return http.build();
+ }
+
+ @Bean
+ public CorsFilter corsFilter() {
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.addAllowedOriginPattern("*"); // 모든 도메인 허용. 필요에 따라 변경
+ config.addAllowedHeader("*");
+ config.addAllowedMethod("*");
+ source.registerCorsConfiguration("/**", config);
+ return new CorsFilter(source);
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/controller/MainController.java b/src/main/java/kaboo/kaboo_auth/controller/MainController.java
new file mode 100644
index 0000000..2665bb2
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/controller/MainController.java
@@ -0,0 +1,24 @@
+package kaboo.kaboo_auth.controller;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class MainController {
+
+ @GetMapping("/")
+ public String mainAPI() {
+ return "누구나 접근가능한 API입니다.";
+ }
+
+ @GetMapping("/test")
+ public String authAPI() {
+ return "권한 Test API 입니다.";
+ }
+
+ @GetMapping("/auth/hello")
+ public String helloAuth(Authentication authentication) {
+ return "인가 받은 사용자 " + authentication.getName() + " 님 환영합니다.";
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/controller/MainControllerAdvice.java b/src/main/java/kaboo/kaboo_auth/controller/MainControllerAdvice.java
new file mode 100644
index 0000000..0d62ae9
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/controller/MainControllerAdvice.java
@@ -0,0 +1,26 @@
+package kaboo.kaboo_auth.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import kaboo.kaboo_auth.domain.dto.response.ResponseDTO;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@RestControllerAdvice
+public class MainControllerAdvice {
+
+ @ExceptionHandler({IllegalStateException.class, UsernameNotFoundException.class})
+ public ResponseEntity> exceptionHandler(Exception e) {
+ log.error("[Kaboo-Auth]: 예외가 발생하였습니다. {}", e.getMessage());
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ true,
+ e.getMessage(),
+ null
+ ));
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/controller/MemberController.java b/src/main/java/kaboo/kaboo_auth/controller/MemberController.java
new file mode 100644
index 0000000..4affd6e
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/controller/MemberController.java
@@ -0,0 +1,86 @@
+package kaboo.kaboo_auth.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import kaboo.kaboo_auth.domain.dto.request.MemberInfoUpdateRequest;
+import kaboo.kaboo_auth.domain.dto.response.MemberInfoResponse;
+import kaboo.kaboo_auth.domain.dto.response.MemberListResponse;
+import kaboo.kaboo_auth.domain.dto.response.ResponseDTO;
+import kaboo.kaboo_auth.service.MemberService;
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/auth/member")
+public class MemberController {
+
+ private final MemberService memberService;
+
+ @GetMapping("/all")
+ public ResponseEntity> getAllMembers() {
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ memberService.getAllMembers()
+ ));
+ }
+
+ @GetMapping("/class/{class}")
+ public ResponseEntity> getClassMembers(
+ @PathVariable(name = "class") int classNum) {
+
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ memberService.getMembersByClassNum(classNum)
+ ));
+ }
+
+ @GetMapping
+ public ResponseEntity> getMemberInfo(
+ @RequestParam(name = "name", defaultValue = "") String koreaName) {
+
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ memberService.getMemberInfoByKoreaName(koreaName)
+ ));
+ }
+
+ @PostMapping
+ public ResponseEntity> updateMemberInfo(
+ @RequestParam(name = "name", defaultValue = "") String koreaName,
+ @RequestBody MemberInfoUpdateRequest request) {
+
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ memberService.updateMemberInfoByKoreaName(koreaName, request)
+ ));
+ }
+
+ @GetMapping("/introduce")
+ public ResponseEntity> getMemberIntroduce(
+ @RequestParam(name = "name", defaultValue = "") String koreaName) {
+
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ memberService.getMemberIntroduceByKoreaName(koreaName)
+ ));
+ }
+
+ @PostMapping("/introduce")
+ public ResponseEntity> updateMemberIntrouce(
+ @RequestParam(name = "name", defaultValue = "") String koreaName,
+ @RequestBody String request) {
+
+ return ResponseEntity.status(HttpStatus.OK)
+ .body(new ResponseDTO<>(
+ memberService.updateMemberIntroduceByKoreaName(koreaName, request)
+ ));
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/Course.java b/src/main/java/kaboo/kaboo_auth/domain/Course.java
new file mode 100644
index 0000000..c5a3a06
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/Course.java
@@ -0,0 +1,13 @@
+package kaboo.kaboo_auth.domain;
+
+public enum Course {
+ AI("GenAI"),
+ FULLSTACK("Fullstack"),
+ CLOUD("Cloud");
+
+ private final String course;
+
+ Course(String course) {
+ this.course = course;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/CustomUserDetails.java b/src/main/java/kaboo/kaboo_auth/domain/CustomUserDetails.java
new file mode 100644
index 0000000..7554366
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/CustomUserDetails.java
@@ -0,0 +1,45 @@
+package kaboo.kaboo_auth.domain;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import kaboo.kaboo_auth.domain.entity.Member;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class CustomUserDetails implements OAuth2User, UserDetails {
+ private final Member member;
+
+ @Override
+ public String getUsername() {
+ return member.getUsername();
+ }
+
+ @Override
+ public String getName() {
+ return member.getKoreaName();
+ }
+
+ @Override
+ public String getPassword() {
+ return member.getPassword();
+ }
+
+ @Override
+ public Map getAttributes() {
+ return null;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ Collection collection = new ArrayList<>();
+ collection.add((GrantedAuthority)() -> member.getRole().toString());
+
+ return collection;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/UserRole.java b/src/main/java/kaboo/kaboo_auth/domain/UserRole.java
new file mode 100644
index 0000000..960c830
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/UserRole.java
@@ -0,0 +1,15 @@
+package kaboo.kaboo_auth.domain;
+
+import lombok.Getter;
+
+@Getter
+public enum UserRole {
+ ROLE_ADMIN("ROLE_ADMIN"),
+ ROLE_USER("ROLE_USER");
+
+ private final String role;
+
+ UserRole(String role) {
+ this.role = role;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/dto/request/MemberInfoUpdateRequest.java b/src/main/java/kaboo/kaboo_auth/domain/dto/request/MemberInfoUpdateRequest.java
new file mode 100644
index 0000000..53ab147
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/dto/request/MemberInfoUpdateRequest.java
@@ -0,0 +1,13 @@
+package kaboo.kaboo_auth.domain.dto.request;
+
+import kaboo.kaboo_auth.domain.Course;
+import lombok.Getter;
+
+@Getter
+public class MemberInfoUpdateRequest {
+ private String koreaName;
+ private String englishName;
+ private int classNum;
+ private Course course;
+
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/dto/response/KakaoResponse.java b/src/main/java/kaboo/kaboo_auth/domain/dto/response/KakaoResponse.java
new file mode 100644
index 0000000..d7bc93e
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/dto/response/KakaoResponse.java
@@ -0,0 +1,72 @@
+package kaboo.kaboo_auth.domain.dto.response;
+
+import java.util.Map;
+
+/** Kakao Response 형태
+ * {
+ * id: int,
+ * connected_at: Date
+ * properties: {
+ * nickname: String
+ * profile_image: URL
+ * thumbnail_image: URL
+ * },
+ * kakao_account: {
+ * profile_nickname_needs_agreement: boolean,
+ * profile_image_needs_agreement: boolean
+ * profile: {
+ * nickname: String
+ * thumbnail_image_url: URL
+ * profile_image_url: URL
+ * is_default_image: boolean
+ * is_default_nickname: boolean
+ * },
+ * has_email=true,
+ * email_needs_agreement=false,
+ * is_email_valid=true,
+ * is_email_verified=true,
+ * email=taejin7824@kakao.com
+ * }
+ *
+ * }
+ */
+
+public class KakaoResponse implements OAuth2Response {
+ private final Map attribute;
+
+ public KakaoResponse(Map attribute) {
+ this.attribute = attribute;
+ }
+
+ @Override
+ public String getProvider() {
+ return "kakao";
+ }
+
+ @Override
+ public String getProviderId() {
+ return attribute.get("id").toString();
+ }
+
+ @Override
+ public String getEmail() {
+ Map kakaoAccount = (Map)attribute.get("kakao_account");
+
+ // "kakao_account" 맵에서 "email" 키의 값을 가져옴
+ if (kakaoAccount != null && kakaoAccount.containsKey("email")) {
+ return kakaoAccount.get("email").toString();
+ }
+ return null; // email이 없을 경우
+ }
+
+ @Override
+ public String getNickname() {
+ Map properties = (Map)attribute.get("properties");
+
+ // "properties" 맵에서 "nickname" 키의 값을 가져옴
+ if (properties != null && properties.containsKey("nickname")) {
+ return properties.get("nickname").toString();
+ }
+ return null; // nickname이 없을 경우
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/dto/response/MemberInfoResponse.java b/src/main/java/kaboo/kaboo_auth/domain/dto/response/MemberInfoResponse.java
new file mode 100644
index 0000000..853943c
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/dto/response/MemberInfoResponse.java
@@ -0,0 +1,20 @@
+package kaboo.kaboo_auth.domain.dto.response;
+
+import kaboo.kaboo_auth.domain.Course;
+import kaboo.kaboo_auth.domain.entity.Member;
+import lombok.Getter;
+
+@Getter
+public class MemberInfoResponse {
+ private final String koreaName;
+ private final String englishName;
+ private final int classNum;
+ private final Course course;
+
+ public MemberInfoResponse(Member member) {
+ this.koreaName = member.getKoreaName();
+ this.englishName = member.getEnglishName();
+ this.classNum = member.getClassNum();
+ this.course = member.getCourse();
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/dto/response/MemberListResponse.java b/src/main/java/kaboo/kaboo_auth/domain/dto/response/MemberListResponse.java
new file mode 100644
index 0000000..2f46aff
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/dto/response/MemberListResponse.java
@@ -0,0 +1,21 @@
+package kaboo.kaboo_auth.domain.dto.response;
+
+import java.util.List;
+
+import kaboo.kaboo_auth.domain.entity.Member;
+import lombok.Getter;
+
+@Getter
+public class MemberListResponse {
+ private final int classNum;
+ private final int memberNum;
+ private final List memberList;
+
+ public MemberListResponse(List members, int classNum) {
+ this.classNum = classNum;
+ this.memberNum = members.size();
+ this.memberList = members.stream()
+ .map(MemberInfoResponse::new)
+ .toList();
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/dto/response/OAuth2Response.java b/src/main/java/kaboo/kaboo_auth/domain/dto/response/OAuth2Response.java
new file mode 100644
index 0000000..145d7ec
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/dto/response/OAuth2Response.java
@@ -0,0 +1,11 @@
+package kaboo.kaboo_auth.domain.dto.response;
+
+public interface OAuth2Response {
+ String getProvider();
+
+ String getProviderId();
+
+ String getEmail();
+
+ String getNickname();
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/dto/response/ResponseDTO.java b/src/main/java/kaboo/kaboo_auth/domain/dto/response/ResponseDTO.java
new file mode 100644
index 0000000..a292021
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/dto/response/ResponseDTO.java
@@ -0,0 +1,18 @@
+package kaboo.kaboo_auth.domain.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class ResponseDTO {
+ private boolean success;
+ private String message;
+ private T data;
+
+ public ResponseDTO(T data) {
+ success = true;
+ message = "요청이 성공적으로 처리되었습니다.";
+ this.data = data;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/entity/Member.java b/src/main/java/kaboo/kaboo_auth/domain/entity/Member.java
new file mode 100644
index 0000000..bbd68c9
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/entity/Member.java
@@ -0,0 +1,67 @@
+package kaboo.kaboo_auth.domain.entity;
+
+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 kaboo.kaboo_auth.domain.Course;
+import kaboo.kaboo_auth.domain.UserRole;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor
+public class Member {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "member_id")
+ private Long id;
+
+ private String username;
+ private String email;
+ private String koreaName;
+ private String englishName;
+ private String password;
+
+ @Column(columnDefinition = "TEXT")
+ private String introduce;
+
+ private int classNum;
+
+ @Enumerated(EnumType.STRING)
+ private Course course;
+
+ @Enumerated(EnumType.STRING)
+ private UserRole role;
+
+ @Builder
+ public Member(String username, String email, String koreaName, String englishName, String password,
+ String introduce,
+ int classNum, Course course, UserRole role) {
+ this.username = username;
+ this.email = email;
+ this.koreaName = koreaName;
+ this.englishName = englishName;
+ this.password = password;
+ this.introduce = introduce;
+ this.classNum = classNum;
+ this.course = course;
+ this.role = role;
+ }
+
+ public void updateInfo(String koreaName, String englishName, int classNum, Course course) {
+ this.koreaName = koreaName;
+ this.englishName = englishName;
+ this.classNum = classNum;
+ this.course = course;
+ }
+
+ public void updateIntroduce(String introduce) {
+ this.introduce = introduce;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/entity/ProfileImage.java b/src/main/java/kaboo/kaboo_auth/domain/entity/ProfileImage.java
new file mode 100644
index 0000000..590c501
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/entity/ProfileImage.java
@@ -0,0 +1,35 @@
+package kaboo.kaboo_auth.domain.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@NoArgsConstructor
+public class ProfileImage {
+ @Id
+ @Column(name = "profile_image_name")
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @OneToOne
+ @JoinColumn(name = "member_id")
+ private Member member;
+
+ @Setter
+ @Column(name = "image_name")
+ private String imageName;
+
+ public ProfileImage(Member member, String imageName) {
+ this.member = member;
+ this.imageName = imageName;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/handler/LoginSuccessHandler.java b/src/main/java/kaboo/kaboo_auth/domain/handler/LoginSuccessHandler.java
new file mode 100644
index 0000000..4bfe757
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/handler/LoginSuccessHandler.java
@@ -0,0 +1,54 @@
+package kaboo.kaboo_auth.domain.handler;
+
+import java.io.IOException;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import kaboo.kaboo_auth.domain.jwt.JwtTokenProvider;
+import kaboo.kaboo_auth.domain.jwt.entity.JwtAccessToken;
+import kaboo.kaboo_auth.domain.jwt.entity.JwtRefreshToken;
+import kaboo.kaboo_auth.domain.jwt.repository.JwtAccessTokenRepository;
+import kaboo.kaboo_auth.domain.jwt.repository.JwtRefreshTokenRepository;
+import lombok.RequiredArgsConstructor;
+
+@Component
+@RequiredArgsConstructor
+public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
+
+ private final JwtAccessTokenRepository jwtAccessTokenRepository;
+ private final JwtRefreshTokenRepository jwtRefreshTokenRepository;
+ private final JwtTokenProvider jwtTokenProvider;
+ private final int accessTokenValidTime = 10 * 60; // 유효기간 : 10분
+ private final int refreshTokenValidTime = 10 * 24 * 60 * 60; // 유효기간 : 10일
+
+ private Cookie createCookie(String key, String value, int maxAge) {
+ Cookie cookie = new Cookie(key, value);
+ cookie.setMaxAge(maxAge);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+
+ return cookie;
+ }
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+ Authentication authentication) throws
+ IOException {
+ String username = authentication.getName();
+ String accessToken = jwtTokenProvider.createAccessToken(username);
+ String refreshToken = jwtTokenProvider.createRefreshToken(username);
+
+ jwtAccessTokenRepository.save(new JwtAccessToken(username, accessToken));
+ jwtRefreshTokenRepository.save(new JwtRefreshToken(username, refreshToken));
+
+ response.addCookie(createCookie("Username", username, refreshTokenValidTime));
+ response.addCookie(createCookie("Authorization", accessToken, accessTokenValidTime));
+ response.addCookie(createCookie("RefreshToken", refreshToken, refreshTokenValidTime));
+ response.sendRedirect("/");
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/jwt/JwtTokenProvider.java b/src/main/java/kaboo/kaboo_auth/domain/jwt/JwtTokenProvider.java
new file mode 100644
index 0000000..37d6007
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/jwt/JwtTokenProvider.java
@@ -0,0 +1,62 @@
+package kaboo.kaboo_auth.domain.jwt;
+
+import java.util.Date;
+
+import javax.crypto.SecretKey;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+
+@Component
+public class JwtTokenProvider {
+
+ private final String accessSecretKey;
+ private final String refreshSecretKey;
+ private final String issuer;
+
+ private final long accessTokenValidTime = 10 * 60 * 1_000L; // 유효기간: 10분
+ private final long refreshTokenValidTime = 10 * 24 * 60 * 60 * 1_000L; // 유효기간: 10일
+
+ public JwtTokenProvider(
+ @Value("${JWT.ACCESS_SECRET_KEY}") String accessSecretKey,
+ @Value("${JWT.REFRESH_SECRET_KEY}") String refreshSecretKey,
+ @Value("${JWT.ISSUER}") String issuer) {
+
+ this.accessSecretKey = accessSecretKey;
+ this.refreshSecretKey = refreshSecretKey;
+ this.issuer = issuer;
+ }
+
+ private SecretKey getSignedKey(String key) {
+ byte[] keyBytes = Decoders.BASE64.decode(key);
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ public String createAccessToken(String username) {
+ Date now = new Date();
+
+ return Jwts.builder()
+ .subject(username)
+ .issuer(issuer)
+ .issuedAt(now)
+ .expiration(new Date(now.getTime() + accessTokenValidTime))
+ .signWith(getSignedKey(accessSecretKey))
+ .compact();
+ }
+
+ public String createRefreshToken(String username) {
+ Date now = new Date();
+
+ return Jwts.builder()
+ .subject(username)
+ .issuer(issuer)
+ .issuedAt(now)
+ .expiration(new Date(now.getTime() + refreshTokenValidTime))
+ .signWith(getSignedKey(refreshSecretKey))
+ .compact();
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/jwt/entity/JwtAccessToken.java b/src/main/java/kaboo/kaboo_auth/domain/jwt/entity/JwtAccessToken.java
new file mode 100644
index 0000000..fa4daf8
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/jwt/entity/JwtAccessToken.java
@@ -0,0 +1,16 @@
+package kaboo.kaboo_auth.domain.jwt.entity;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RedisHash(value = "JwtAccessToken", timeToLive = 60 * 10) // 유효기간 : 10분
+@RequiredArgsConstructor
+public class JwtAccessToken {
+ @Id
+ private final String username;
+ private final String accessToken;
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/jwt/entity/JwtRefreshToken.java b/src/main/java/kaboo/kaboo_auth/domain/jwt/entity/JwtRefreshToken.java
new file mode 100644
index 0000000..ca61a61
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/jwt/entity/JwtRefreshToken.java
@@ -0,0 +1,16 @@
+package kaboo.kaboo_auth.domain.jwt.entity;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RedisHash(value = "JwtRefreshToken", timeToLive = 60 * 60 * 24 * 10) // 유효기간 : 10일
+@RequiredArgsConstructor
+public class JwtRefreshToken {
+ @Id
+ private final String username;
+ private final String refreshToken;
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/jwt/filter/JwtFilter.java b/src/main/java/kaboo/kaboo_auth/domain/jwt/filter/JwtFilter.java
new file mode 100644
index 0000000..c62d302
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/jwt/filter/JwtFilter.java
@@ -0,0 +1,116 @@
+package kaboo.kaboo_auth.domain.jwt.filter;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import kaboo.kaboo_auth.domain.CustomUserDetails;
+import kaboo.kaboo_auth.domain.entity.Member;
+import kaboo.kaboo_auth.domain.jwt.JwtTokenProvider;
+import kaboo.kaboo_auth.domain.jwt.entity.JwtAccessToken;
+import kaboo.kaboo_auth.domain.jwt.entity.JwtRefreshToken;
+import kaboo.kaboo_auth.domain.jwt.repository.JwtAccessTokenRepository;
+import kaboo.kaboo_auth.domain.jwt.repository.JwtRefreshTokenRepository;
+import kaboo.kaboo_auth.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtFilter extends OncePerRequestFilter {
+
+ private final MemberRepository memberRepository;
+ private final JwtAccessTokenRepository jwtAccessTokenRepository;
+ private final JwtRefreshTokenRepository jwtRefreshTokenRepository;
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ // 쿠키가 없다면 다음 로직으로
+ Cookie[] cookies = request.getCookies();
+ if (cookies == null) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ String username = null, cookieAccessToken = null, cookieRefreshToken = null;
+ for (Cookie cookie : cookies) {
+ switch (cookie.getName()) {
+ case "Username" -> username = cookie.getValue();
+ case "Authorization" -> cookieAccessToken = cookie.getValue();
+ case "RefreshToken" -> cookieRefreshToken = cookie.getValue();
+ }
+ }
+
+ // 쿠키에 토큰이 있는지 검사
+ if (cookieAccessToken == null || username == null) {
+ chain.doFilter(request, response);
+ return;
+ }
+ Optional jwtAccessToken = jwtAccessTokenRepository.findById(username);
+
+ // 요청한 사용자정보로 redis 에 accessToken 이 존재할 때
+ if (jwtAccessToken.isPresent()) {
+ JwtAccessToken accessToken = jwtAccessToken.get();
+
+ // 토큰에서 꺼낸 사용자 id와 전달 받은 사용자 id가 같고
+ // Redis에 저장된 AccessToken과 전달 받은 AccessToken이 일치할 때
+ if (accessToken.getUsername().equals(username) && accessToken.getAccessToken()
+ .equals(cookieAccessToken)) {
+ Member member = memberRepository.findByUsername(username) // 해당 사용자 id 로 사용자 정보가 있는지 찾기
+ .orElseThrow(() -> {
+ // 로그인에 성공한 사람들(사용자 정보가 있는 경우)만 토큰을 부여받기 때문에 예외를 던진다면 데이터베이스 오류이거나 로그인 로직에 버그가 있는 것
+ return new UsernameNotFoundException("해당 사용자 정보를 찾을 수 없습니다. [DB OR 로그인 로직 버그]");
+ });
+
+ CustomUserDetails userDetails = new CustomUserDetails(member);
+ Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
+ userDetails.getAuthorities());
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ chain.doFilter(request, response);
+
+ return;
+ }
+ }
+
+ if (cookieRefreshToken != null) { // 리프레시 토큰이 전달되었다면
+ Optional jwtRefreshToken = jwtRefreshTokenRepository.findById(username);
+ if (jwtRefreshToken.isPresent()) { // Redis에 리프레시 토큰이 존재한다면
+ JwtRefreshToken refreshToken = jwtRefreshToken.get();
+
+ // 토큰에서 꺼낸 사용자 id와 전달 받은 사용자 id가 같고
+ // Redis에 저장된 AccessToken과 전달 받은 AccessToken이 일치할 때
+ if (refreshToken.getUsername().equals(username) && refreshToken.getRefreshToken()
+ .equals(cookieRefreshToken)) {
+ JwtAccessToken newAccessToken = new JwtAccessToken(username,
+ jwtTokenProvider.createAccessToken(username));
+
+ jwtAccessTokenRepository.save(newAccessToken);
+ response.addCookie(createCookie("Authorization", newAccessToken.getAccessToken()));
+ }
+ }
+ }
+ }
+
+ private Cookie createCookie(String key, String value) {
+ Cookie cookie = new Cookie(key, value);
+ cookie.setMaxAge(10 * 60);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ return cookie;
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/jwt/repository/JwtAccessTokenRepository.java b/src/main/java/kaboo/kaboo_auth/domain/jwt/repository/JwtAccessTokenRepository.java
new file mode 100644
index 0000000..4b9328d
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/jwt/repository/JwtAccessTokenRepository.java
@@ -0,0 +1,8 @@
+package kaboo.kaboo_auth.domain.jwt.repository;
+
+import org.springframework.data.repository.CrudRepository;
+
+import kaboo.kaboo_auth.domain.jwt.entity.JwtAccessToken;
+
+public interface JwtAccessTokenRepository extends CrudRepository {
+}
diff --git a/src/main/java/kaboo/kaboo_auth/domain/jwt/repository/JwtRefreshTokenRepository.java b/src/main/java/kaboo/kaboo_auth/domain/jwt/repository/JwtRefreshTokenRepository.java
new file mode 100644
index 0000000..7411367
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/domain/jwt/repository/JwtRefreshTokenRepository.java
@@ -0,0 +1,8 @@
+package kaboo.kaboo_auth.domain.jwt.repository;
+
+import org.springframework.data.repository.CrudRepository;
+
+import kaboo.kaboo_auth.domain.jwt.entity.JwtRefreshToken;
+
+public interface JwtRefreshTokenRepository extends CrudRepository {
+}
diff --git a/src/main/java/kaboo/kaboo_auth/repository/MemberRepository.java b/src/main/java/kaboo/kaboo_auth/repository/MemberRepository.java
new file mode 100644
index 0000000..22b742d
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/repository/MemberRepository.java
@@ -0,0 +1,16 @@
+package kaboo.kaboo_auth.repository;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import kaboo.kaboo_auth.domain.entity.Member;
+
+public interface MemberRepository extends JpaRepository {
+ Optional findByUsername(String username);
+
+ List findByClassNum(int classNum);
+
+ Optional findByKoreaName(String koreaName);
+}
diff --git a/src/main/java/kaboo/kaboo_auth/repository/ProfileImageRepository.java b/src/main/java/kaboo/kaboo_auth/repository/ProfileImageRepository.java
new file mode 100644
index 0000000..d0e6c46
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/repository/ProfileImageRepository.java
@@ -0,0 +1,8 @@
+package kaboo.kaboo_auth.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import kaboo.kaboo_auth.domain.entity.ProfileImage;
+
+public interface ProfileImageRepository extends JpaRepository {
+}
diff --git a/src/main/java/kaboo/kaboo_auth/service/CustomOAuth2Service.java b/src/main/java/kaboo/kaboo_auth/service/CustomOAuth2Service.java
new file mode 100644
index 0000000..87eb4b4
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/service/CustomOAuth2Service.java
@@ -0,0 +1,67 @@
+package kaboo.kaboo_auth.service;
+
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+
+import kaboo.kaboo_auth.domain.CustomUserDetails;
+import kaboo.kaboo_auth.domain.UserRole;
+import kaboo.kaboo_auth.domain.dto.response.KakaoResponse;
+import kaboo.kaboo_auth.domain.dto.response.OAuth2Response;
+import kaboo.kaboo_auth.domain.entity.Member;
+import kaboo.kaboo_auth.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class CustomOAuth2Service extends DefaultOAuth2UserService {
+ private final MemberRepository memberRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ @Value("${AUTH.PASSWORD_POSTFIX}")
+ String passwordPostfix;
+
+ @Override
+ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
+ OAuth2User oauth2User = super.loadUser(userRequest);
+ OAuth2Response response;
+ if (userRequest.getClientRegistration().getRegistrationId().equals("kakao")) {
+ response = new KakaoResponse(oauth2User.getAttributes());
+ } else {
+ throw new IllegalArgumentException("사용할 수 없는 인증방법입니다.");
+ }
+
+ String provider = response.getProvider();
+ String providerId = response.getProviderId();
+
+ // 중복이 발생하지 않도록 provider와 providerId를 조합
+ String username = provider + "_" + providerId;
+ String email = response.getEmail();
+ String nickname = response.getNickname();
+
+ Optional byUsername = memberRepository.findByUsername(username);
+ Member member = null;
+ if (byUsername.isEmpty()) {
+ String rawPassword = username + passwordPostfix;
+ member = Member.builder()
+ .username(username)
+ .koreaName(nickname)
+ .email(email)
+ .password(passwordEncoder.encode(rawPassword))
+ .role(UserRole.ROLE_USER)
+ .build();
+
+ memberRepository.save(member);
+ } else {
+ member = byUsername.get();
+ }
+
+ return new CustomUserDetails(member);
+ }
+}
diff --git a/src/main/java/kaboo/kaboo_auth/service/MemberService.java b/src/main/java/kaboo/kaboo_auth/service/MemberService.java
new file mode 100644
index 0000000..ac0b6b6
--- /dev/null
+++ b/src/main/java/kaboo/kaboo_auth/service/MemberService.java
@@ -0,0 +1,66 @@
+package kaboo.kaboo_auth.service;
+
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import kaboo.kaboo_auth.domain.dto.request.MemberInfoUpdateRequest;
+import kaboo.kaboo_auth.domain.dto.response.MemberInfoResponse;
+import kaboo.kaboo_auth.domain.dto.response.MemberListResponse;
+import kaboo.kaboo_auth.domain.entity.Member;
+import kaboo.kaboo_auth.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class MemberService {
+ private final MemberRepository memberRepository;
+
+ public Member getMemberByUsername(String username) {
+ return memberRepository.findByUsername(username).orElseThrow(() ->
+ new IllegalStateException("존재하지 않는 ID 입니다.")
+ );
+ }
+
+ public MemberListResponse getAllMembers() {
+ List members = memberRepository.findAll();
+ return new MemberListResponse(members, 0);
+ }
+
+ public MemberListResponse getMembersByClassNum(int classNum) {
+ List members = memberRepository.findByClassNum(classNum);
+ return new MemberListResponse(members, classNum);
+ }
+
+ public MemberInfoResponse getMemberInfoByKoreaName(String koreaName) {
+ return new MemberInfoResponse(
+ getMember(koreaName));
+ }
+
+ @Transactional
+ public MemberInfoResponse updateMemberInfoByKoreaName(String koreaName, MemberInfoUpdateRequest request) {
+ Member member = getMember(koreaName);
+
+ member.updateInfo(request.getKoreaName(), request.getEnglishName(), request.getClassNum(), request.getCourse());
+
+ return new MemberInfoResponse(member);
+ }
+
+ public String getMemberIntroduceByKoreaName(String koreaName) {
+ return getMember(koreaName).getIntroduce();
+ }
+
+ @Transactional
+ public String updateMemberIntroduceByKoreaName(String koreaName, String request) {
+ Member member = getMember(koreaName);
+ member.updateIntroduce(request);
+
+ return member.getIntroduce();
+ }
+
+ private Member getMember(String koreaName) {
+ return memberRepository.findByKoreaName(koreaName)
+ .orElseThrow(() -> new IllegalStateException(koreaName + "을 찾을 수 없습니다. 다시 한번 확인해주세요."));
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index 84dcfeb..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=kaboo-auth
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..8c2d5c5
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,8 @@
+spring:
+ application:
+ name: kaboo-auth
+ config:
+ import:
+ - oauth.yml
+ - jwt.yml
+ - database.yml
diff --git a/src/test/java/kaboo/kaboo_auth/KabooAuthApplicationTests.java b/src/test/java/kaboo/kaboo_auth/KabooAuthApplicationTests.java
index 70093fc..af9cdf9 100644
--- a/src/test/java/kaboo/kaboo_auth/KabooAuthApplicationTests.java
+++ b/src/test/java/kaboo/kaboo_auth/KabooAuthApplicationTests.java
@@ -4,6 +4,7 @@
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
+@ActiveProfiles("test")
class KabooAuthApplicationTests {
@Test
diff --git a/src/test/java/kaboo/kaboo_auth/repository/MemberRepositoryTest.java b/src/test/java/kaboo/kaboo_auth/repository/MemberRepositoryTest.java
new file mode 100644
index 0000000..b1c8d0f
--- /dev/null
+++ b/src/test/java/kaboo/kaboo_auth/repository/MemberRepositoryTest.java
@@ -0,0 +1,61 @@
+package kaboo.kaboo_auth.repository;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.transaction.annotation.Transactional;
+
+import kaboo.kaboo_auth.domain.entity.Member;
+
+@DataJpaTest
+@Transactional
+@DisplayName("Member Repository Test")
+class MemberRepositoryTest {
+ @Autowired
+ MemberRepository memberRepository;
+
+ @Test
+ @DisplayName("DB 저장 Test")
+ void saveMemberTest() {
+ // Given
+ Member member = Member.builder().username("Alice").englishName("Alice").password("1234").build();
+ memberRepository.save(member);
+
+ // When
+ Member result = memberRepository.findById(member.getId()).get();
+
+ // Then
+ assertEquals(result, member);
+ }
+
+ @Test
+ @DisplayName("Username으로 찾기 성공 Test")
+ void findByUsername_Success() {
+ // Given
+ Member member1 = Member.builder().username("Alice").englishName("Alice").password("1234").build();
+ memberRepository.save(member1);
+
+ // When
+ Member result = memberRepository.findByUsername("Alice").get();
+
+ // Then
+ assertEquals(result, member1);
+ }
+
+ @Test
+ @DisplayName("Username 존재하지 않을 때 Test")
+ void findByUsername_Failure() {
+ // Given
+
+ // When
+ Optional result = memberRepository.findByUsername("Alice");
+
+ // Then
+ assertEquals(result, Optional.empty());
+ }
+}
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
new file mode 100644
index 0000000..01468af
--- /dev/null
+++ b/src/test/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:test;MODE=MariaDB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+ username: sa
+ password:
+ driverClassName: org.h2.Driver
+ jpa:
+ database-platform: org.hibernate.dialect.H2Dialect
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
\ No newline at end of file