diff --git a/.gitignore b/.gitignore
index caa32e6..55ccd32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,9 @@
.idea/
-*.iml
\ No newline at end of file
+*.iml
+### yml ###
+application.yml
+
+
+.env
+
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index b7a35ad..0e493d9 100644
--- a/README.md
+++ b/README.md
@@ -10,3 +10,4 @@
|
|
|
|
|
|
| 윤민섭 | 김혜림 | 박채연 | 박세웅 | 김민주 |
| [Minsub](https://github.com/minsubyun1) | [kimhyerim01](https://github.com/kimhyerim01) | [yeonchaepark](https://github.com/yeonchaepark) | [hardwoong](https://github.com/hardwoong) | [calla1102](https://github.com/calla1102) |
+
diff --git a/retrigger.txt b/retrigger.txt
new file mode 100644
index 0000000..ab6a307
--- /dev/null
+++ b/retrigger.txt
@@ -0,0 +1,2 @@
+// retrigger CI after base branch change
+//ABCD CI after base branch change
diff --git a/server/.gitignore b/server/.gitignore
index c2065bc..a063734 100644
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -35,3 +35,7 @@ out/
### VS Code ###
.vscode/
+
+
+### yml
+application.yml
\ No newline at end of file
diff --git a/server/build.gradle b/server/build.gradle
index 5116a3f..30bb27d 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -27,12 +27,23 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ implementation("io.jsonwebtoken:jjwt-api:0.12.4")
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.4")
+ runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.4")
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+ implementation 'io.github.cdimascio:dotenv-java:3.0.0'
+ implementation 'me.paulschwarz:spring-dotenv:4.0.0'
}
tasks.named('test') {
diff --git a/server/src/main/java/com/soopgyeol/api/common/dto/NicknameUpdateRequest.java b/server/src/main/java/com/soopgyeol/api/common/dto/NicknameUpdateRequest.java
new file mode 100644
index 0000000..ecdf725
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/common/dto/NicknameUpdateRequest.java
@@ -0,0 +1,13 @@
+package com.soopgyeol.api.common.dto;
+
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+
+@Getter
+public class NicknameUpdateRequest {
+ @Size(min = 2, max = 12, message = "닉네임은 2자 이상 12자 이하로 입력해주세요.")
+ @Pattern(regexp = ".*[a-zA-Z가-힣]+.*", message = "닉네임에는 한글 또는 영문자가 최소 1자 이상 포함되어야 합니다.")
+ private String nickname;
+
+}
diff --git a/server/src/main/java/com/soopgyeol/api/common/dto/NicknameUpdateResponse.java b/server/src/main/java/com/soopgyeol/api/common/dto/NicknameUpdateResponse.java
new file mode 100644
index 0000000..3260ca0
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/common/dto/NicknameUpdateResponse.java
@@ -0,0 +1,10 @@
+package com.soopgyeol.api.common.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class NicknameUpdateResponse {
+ private String nickname;
+}
diff --git a/server/src/main/java/com/soopgyeol/api/common/exception/InsufficientBalanceException.java b/server/src/main/java/com/soopgyeol/api/common/exception/InsufficientBalanceException.java
new file mode 100644
index 0000000..c255b86
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/common/exception/InsufficientBalanceException.java
@@ -0,0 +1,7 @@
+package com.soopgyeol.api.common.exception;
+
+public class InsufficientBalanceException extends RuntimeException {
+ public InsufficientBalanceException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/soopgyeol/api/common/exception/ItemAlreadyOwnedException.java b/server/src/main/java/com/soopgyeol/api/common/exception/ItemAlreadyOwnedException.java
new file mode 100644
index 0000000..b76ee63
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/common/exception/ItemAlreadyOwnedException.java
@@ -0,0 +1,8 @@
+package com.soopgyeol.api.common.exception;
+
+public class ItemAlreadyOwnedException extends RuntimeException {
+ public ItemAlreadyOwnedException(String message) {
+ super(message);
+ }
+}
+
diff --git a/server/src/main/java/com/soopgyeol/api/config/DevDataInitializer.java b/server/src/main/java/com/soopgyeol/api/config/DevDataInitializer.java
new file mode 100644
index 0000000..71f0234
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/config/DevDataInitializer.java
@@ -0,0 +1,33 @@
+package com.soopgyeol.api.config;
+
+import com.soopgyeol.api.domain.user.Role;
+import com.soopgyeol.api.domain.user.SocialLoginType;
+import com.soopgyeol.api.domain.user.User;
+import com.soopgyeol.api.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.boot.CommandLineRunner;
+
+@Component
+@RequiredArgsConstructor
+public class DevDataInitializer implements CommandLineRunner{
+
+ private final UserRepository userRepository;
+
+ @Override
+ public void run(String... args) {
+ if(userRepository.count() == 0) {
+ User testUser = User.builder()
+ .nickname("테스트 유저")
+ .email("test@example.com")
+ .password("1234")
+ .role(Role.USER)
+ .provider(SocialLoginType.GOOGLE)
+ .build();
+
+ userRepository.save(testUser);
+
+ System.out.println("테스트 유저가 자동 등록되었습니다. ID: " + testUser.getId());
+ }
+ }
+}
diff --git a/server/src/main/java/com/soopgyeol/api/config/DotEnvConfig.java b/server/src/main/java/com/soopgyeol/api/config/DotEnvConfig.java
new file mode 100644
index 0000000..72fcd69
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/config/DotEnvConfig.java
@@ -0,0 +1,17 @@
+package com.soopgyeol.api.config;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class DotEnvConfig {
+
+ @Bean
+ public Dotenv dotenv() {
+ return Dotenv.configure()
+ .directory(System.getProperty("user.dir")) // 👈 명확하게 루트 경로 지정
+ .ignoreIfMissing()
+ .load();
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/soopgyeol/api/config/PasswordEncoderConfig.java b/server/src/main/java/com/soopgyeol/api/config/PasswordEncoderConfig.java
new file mode 100644
index 0000000..a552b09
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/config/PasswordEncoderConfig.java
@@ -0,0 +1,15 @@
+package com.soopgyeol.api.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/server/src/main/java/com/soopgyeol/api/config/SecurityConfig.java b/server/src/main/java/com/soopgyeol/api/config/SecurityConfig.java
new file mode 100644
index 0000000..d4aaee4
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/config/SecurityConfig.java
@@ -0,0 +1,65 @@
+package com.soopgyeol.api.config;
+
+import com.soopgyeol.api.service.jwt.JwtAuthFilter;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.IpAddressAuthorizationManager;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtAuthFilter jwtAuthFilter;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+
+ http.csrf(csrf -> csrf.disable());
+
+
+ http.sessionManagement(sm -> sm
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+ http.authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/v1/auth/dev-login").permitAll()
+ .requestMatchers(
+ "/oauth2/**",
+ "/login/oauth2/**",
+ "/login-success",
+ "/auth/oauth2/**",
+ "/favicon.ico",
+ "/auth/login",
+ "/oauth2/google/code-log",
+ "/api/v1/auth/oauth/oauth2/**",
+ "/api/v1/auth/oauth/**"
+ ).permitAll()
+ .anyRequest().authenticated()
+ );
+
+
+
+
+
+ http.exceptionHandling(eh -> eh
+ .authenticationEntryPoint(
+ (req, res, ex) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED)));
+
+ http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+
+}
diff --git a/server/src/main/java/com/soopgyeol/api/config/auth/CustomUserDetails.java b/server/src/main/java/com/soopgyeol/api/config/auth/CustomUserDetails.java
new file mode 100644
index 0000000..b7d5f2e
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/config/auth/CustomUserDetails.java
@@ -0,0 +1,62 @@
+package com.soopgyeol.api.config.auth;
+
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+
+
+public class CustomUserDetails implements UserDetails {
+ private final Long userId;
+ private final String username;
+ private final String password;
+ private final Collection extends GrantedAuthority> authorities;
+
+ public CustomUserDetails(Long userId, String username, String password, Collection extends GrantedAuthority> authorities) {
+ this.userId = userId;
+ this.username = username;
+ this.password = password;
+ this.authorities = authorities;
+ }
+
+ // userId getter
+ public Long getUserId() {
+ return userId;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return authorities;
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/server/src/main/java/com/soopgyeol/api/controller/AuthController.java b/server/src/main/java/com/soopgyeol/api/controller/AuthController.java
new file mode 100644
index 0000000..f92ac6b
--- /dev/null
+++ b/server/src/main/java/com/soopgyeol/api/controller/AuthController.java
@@ -0,0 +1,103 @@
+package com.soopgyeol.api.controller;
+
+import com.soopgyeol.api.dto.oauth.OAuthLoginRequest;
+import com.soopgyeol.api.dto.oauth.OAuthLoginResponse;
+import com.soopgyeol.api.service.auth.GoogleOauth;
+import com.soopgyeol.api.service.auth.KakaoOauth;
+import com.soopgyeol.api.service.auth.OAuthService;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.Map;
+
+//임시 토큰 코드 사용 시 활성화
+//import com.soopgyeol.api.domain.user.User;
+//import com.soopgyeol.api.repository.UserRepository;
+//import com.soopgyeol.api.service.jwt.JwtProvider;
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/auth/oauth")
+public class AuthController {
+
+ private final OAuthService oAuthService;
+ private final GoogleOauth googleOauth;
+ private final KakaoOauth kakaoOauth;
+
+ @PostMapping("/login")
+ public OAuthLoginResponse login(@RequestBody OAuthLoginRequest request) {
+ return oAuthService.login(request);
+ }
+
+ @GetMapping("/oauth2/google/url")
+ public ResponseEntity