diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index e30a588..11efac7 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -12,15 +12,15 @@ env:
jobs:
build-with-gradle:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - name: JDK 17 설정
- uses: actions/setup-java@v4
+ - name: JDK 21 설정
+ uses: actions/setup-java@v5
with:
- java-version: '17'
+ java-version: '21'
distribution: 'corretto'
- name: applications.yml 환경변수 설정
@@ -42,8 +42,8 @@ jobs:
- name: 도커 이미지 빌드 및 푸시
run: |
- docker build -t ayeonii/leets-be:latest .
- docker push ayeonii/leets-be:latest
+ docker build -t leetsland/leets-be:latest .
+ docker push leetsland/leets-be:latest
deploy-dev:
needs: build-with-gradle
diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml
index 5a4a480..7fa3bfb 100644
--- a/.github/workflows/pr-ci.yml
+++ b/.github/workflows/pr-ci.yml
@@ -2,7 +2,7 @@ name: PR Test
on:
pull_request:
- branches: [ "main", "develop", "master" ]
+ branches: [ "main", "develop" ]
permissions:
contents: read
@@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v5
with:
- java-version: '17'
+ java-version: '21'
distribution: 'corretto'
- name: Setup Gradle
diff --git a/Dockerfile b/Dockerfile
index f5edc52..8c929e5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM eclipse-temurin:17-jre
+FROM amazoncorretto:21
ARG JAR_FILE=./build/libs/leets-0.0.1-SNAPSHOT.jar
diff --git a/README.md b/README.md
index 28a4f7f..b00e33d 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,23 @@
-# [Leets](https://leets.land)
+# [Leets](https://leets.land) : 함께 도전하며 우리의 가치를 증명하는 곳
-- 가천대학교 IT 학술 동아리 `Leets`의 활동과 모집을 위한 홈페이지 레포지토리입니다.
-- Elite에서 파생된 단어 Leet은 엘리트의 의미를 담고 있습니다.
-- `Leets`는 여러 엘리트가 모인 공동체입니다.
+
+
-
+가천대학교 IT 창업 동아리 **Leets**의 활동과 모집을 위한 랜딩 레포지토리입니다.
-# 기술스택
-+ 프레임워크 : SpringBoot 3.0.9
-+ 언어 : Java 17
-+ 데이터베이스 : MySQL
-+ 인프라 : AWS EC2
-+ CI/CD: GitHub Actions
+Leets는 가천대학교 내에서 IT 서비스에 관심을 가진 이들의 첫 도전이 되고, 세상을 넓게 바라볼 수 있는 시야를 만들어주는 동아리가 되고자 합니다.
-# Environment
-```
-# Cors 관련 환경변수
-CORS_ORIGIN_DEVELOPMENT=
-CORS_ORIGIN_PRODUCTION=
-
-# Database 관련 환경변수
-DATABASE_PASSWORD=
-DATABASE_URL=
-DATABASE_USERNAME=
-
-# Oauth2 관련 환경변수
-GOOGLE_AUTH_URL=
-GOOGLE_ID=
-GOOGLE_LOGIN_URL=
-GOOGLE_PASSWORD=
-GOOGLE_REDIRECT_URL=
-
-# Jwt 관련 환경변수
-JWT_ACCESS_SECRET=
-JWT_REFRESH_SECRET=
-
-# 메일 관련 환경변수
-MAIL_HOST=
-MAIL_USERNAME=
-
-# Url 관련 환경변수
-TARGET_URL_DEV=
-TARGET_URL_PROD=
-```
-
+
+
+
+
+
+
-# 관련 Repository
-[Leets 공식 홈페이지 프론트엔드](https://github.com/Leets-Official/Leets-FE)
+## 🔗 관련 Repository
+
+- [Leets 공식 홈페이지 프론트엔드](https://github.com/Leets-Official/Leets-FE)
diff --git a/build.gradle.kts b/build.gradle.kts
index 39ee1de..adf18a2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,5 +1,4 @@
plugins {
- id("java")
id("org.springframework.boot") version "4.0.0"
id("io.spring.dependency-management") version "1.1.6"
id("org.jetbrains.kotlin.jvm") version "2.2.0"
@@ -10,17 +9,6 @@ plugins {
group = "land"
version = "0.0.1-SNAPSHOT"
-java {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
-}
-
-configurations {
- compileOnly {
- extendsFrom(configurations.annotationProcessor.get())
- }
-}
-
repositories {
mavenCentral()
}
@@ -35,9 +23,6 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
- compileOnly("org.projectlombok:lombok")
- annotationProcessor("org.projectlombok:lombok")
-
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
@@ -52,6 +37,8 @@ dependencies {
implementation("com.google.api-client:google-api-client:2.8.1")
+ implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
+
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
testImplementation("com.h2database:h2")
@@ -65,20 +52,16 @@ tasks.named("test") {
useJUnitPlatform()
}
-tasks.withType().configureEach {
- options.generatedSourceOutputDirectory.set(
- layout.buildDirectory.dir("generated/sources/annotationProcessor/java/main")
- )
-}
-
tasks.withType().configureEach {
failOnNoDiscoveredTests = false
}
kotlin {
compilerOptions {
- freeCompilerArgs.addAll("-Xjsr305=strict")
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
- freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property"))
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
+ freeCompilerArgs.addAll(
+ "-Xjsr305=strict",
+ "-Xannotation-default-target=param-property"
+ )
}
}
diff --git a/src/main/java/land/leets/domain/auth/AdminAuthDetailsService.java b/src/main/java/land/leets/domain/auth/AdminAuthDetailsService.java
deleted file mode 100644
index 6fe3a47..0000000
--- a/src/main/java/land/leets/domain/auth/AdminAuthDetailsService.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package land.leets.domain.auth;
-
-import land.leets.domain.admin.domain.Admin;
-import land.leets.domain.admin.domain.repository.AdminRepository;
-import land.leets.domain.shared.AuthRole;
-import land.leets.global.error.ErrorCode;
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import org.springframework.stereotype.Service;
-
-@RequiredArgsConstructor
-@Service
-public class AdminAuthDetailsService implements UserDetailsService {
-
- private final AdminRepository adminRepository;
-
- @Override
- public AuthDetails loadUserByUsername(String sub) throws UsernameNotFoundException {
- Admin admin = this.adminRepository
- .findByUsername(sub)
- .orElseThrow(() -> new UsernameNotFoundException(ErrorCode.ADMIN_NOT_FOUND.getMessage()));
- return new AuthDetails(admin.getId(), admin.getUsername(), AuthRole.ROLE_ADMIN);
- }
-}
diff --git a/src/main/java/land/leets/domain/auth/AuthDetails.java b/src/main/java/land/leets/domain/auth/AuthDetails.java
deleted file mode 100644
index d69494f..0000000
--- a/src/main/java/land/leets/domain/auth/AuthDetails.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package land.leets.domain.auth;
-
-import land.leets.domain.shared.AuthRole;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.userdetails.UserDetails;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.UUID;
-
-public class AuthDetails implements UserDetails {
-
- private final UUID uid;
- private final String email;
- private final AuthRole role;
-
- public AuthDetails(UUID uid, String email, AuthRole role) {
- this.uid = uid;
- this.email = email;
- this.role = role;
- }
-
- public UUID getUid() {
- return uid;
- }
-
- public String getEmail() {
- return email;
- }
-
- public AuthRole getRole() {
- return role;
- }
-
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
- return Collections.singletonList(new SimpleGrantedAuthority(role.getRole()));
- }
-
- @Override
- public String getPassword() {
- return null;
- }
-
- @Override
- public String getUsername() {
- return email;
- }
-
- @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/src/main/java/land/leets/domain/auth/AuthService.java b/src/main/java/land/leets/domain/auth/AuthService.java
deleted file mode 100644
index f272646..0000000
--- a/src/main/java/land/leets/domain/auth/AuthService.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package land.leets.domain.auth;
-
-import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
-import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
-import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
-import com.google.api.client.http.javanet.NetHttpTransport;
-import com.google.api.client.json.gson.GsonFactory;
-import land.leets.domain.auth.exception.PermissionDeniedException;
-import land.leets.domain.auth.presentation.dto.OAuthTokenDto;
-import land.leets.domain.user.domain.User;
-import land.leets.domain.user.domain.repository.UserRepository;
-import land.leets.global.jwt.exception.InvalidTokenException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-@Service
-public class AuthService {
-
- private final String googleAuthUrl;
- private final String googleRedirectUrl;
- private final String googleClientId;
- private final String googleClientPassword;
- private final UserRepository userRepository;
-
- @Autowired
- public AuthService(@Value("${google.auth.url}") String googleAuthUrl,
- @Value("${google.redirect.url}") String googleRedirectUrl,
- @Value("${spring.security.oauth2.client.registration.google.client-id}") String googleClientId,
- @Value("${spring.security.oauth2.client.registration.google.client-secret}") String googleClientPassword,
- UserRepository userRepository) {
- this.googleAuthUrl = googleAuthUrl;
- this.googleRedirectUrl = googleRedirectUrl;
- this.googleClientId = googleClientId;
- this.googleClientPassword = googleClientPassword;
- this.userRepository = userRepository;
- }
-
- public User getGoogleToken(String code) throws GeneralSecurityException, IOException {
-
- RestTemplate restTemplate = new RestTemplate();
- Map params = new HashMap<>();
-
- params.put("code", code);
- params.put("client_id", googleClientId);
- params.put("client_secret", googleClientPassword);
- params.put("redirect_uri", googleRedirectUrl);
- params.put("grant_type", "authorization_code");
-
- ResponseEntity responseEntity = restTemplate.postForEntity(googleAuthUrl, params, OAuthTokenDto.class);
- if (responseEntity.getStatusCode() != HttpStatus.OK || responseEntity.getBody() == null) {
- throw new PermissionDeniedException();
- }
-
- String idToken = responseEntity.getBody().getId_token();
- return getUser(idToken);
- }
-
-
- public User getUser(String idToken) throws GeneralSecurityException, IOException {
-
- final GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), GsonFactory.getDefaultInstance())
- .setAudience(Collections.singletonList(googleClientId))
- .build();
-
- GoogleIdToken googleIdToken = verifier.verify(idToken);
- if (idToken == null) {
- throw new InvalidTokenException();
- }
-
- Payload payload = googleIdToken.getPayload();
-
- String userId = payload.getSubject();
- Optional bySub = userRepository.findBySub(userId);
-
- if (bySub.isPresent()) {
- return bySub.get();
- }
-
- User user = new User(
- null,
- (String) payload.get("name"),
- null,
- payload.getEmail(),
- userId,
- null
- );
-
- return userRepository.save(user);
- }
-}
diff --git a/src/main/java/land/leets/domain/auth/UserAuthDetailsService.java b/src/main/java/land/leets/domain/auth/UserAuthDetailsService.java
deleted file mode 100644
index e2e27bf..0000000
--- a/src/main/java/land/leets/domain/auth/UserAuthDetailsService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package land.leets.domain.auth;
-
-import land.leets.domain.shared.AuthRole;
-import land.leets.domain.user.domain.User;
-import land.leets.domain.user.domain.repository.UserRepository;
-import land.leets.global.error.ErrorCode;
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class UserAuthDetailsService implements UserDetailsService {
- private final UserRepository userRepository;
-
- @Override
- public AuthDetails loadUserByUsername(String email) throws UsernameNotFoundException {
- User user = this.userRepository
- .findByEmail(email)
- .orElseThrow(() -> new UsernameNotFoundException(ErrorCode.USER_NOT_FOUND.getMessage()));
- return new AuthDetails(user.getId(), user.getEmail(), AuthRole.ROLE_USER);
- }
-}
diff --git a/src/main/java/land/leets/domain/auth/exception/PermissionDeniedException.java b/src/main/java/land/leets/domain/auth/exception/PermissionDeniedException.java
deleted file mode 100644
index 38bb7b5..0000000
--- a/src/main/java/land/leets/domain/auth/exception/PermissionDeniedException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package land.leets.domain.auth.exception;
-
-
-import land.leets.global.error.ErrorCode;
-import land.leets.global.error.exception.ServiceException;
-
-public class PermissionDeniedException extends ServiceException {
- public PermissionDeniedException() {
- super(ErrorCode.PERMISSION_DENIED);
- }
-}
diff --git a/src/main/java/land/leets/domain/auth/presentation/AuthController.java b/src/main/java/land/leets/domain/auth/presentation/AuthController.java
deleted file mode 100644
index 923dd5c..0000000
--- a/src/main/java/land/leets/domain/auth/presentation/AuthController.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package land.leets.domain.auth.presentation;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Content;
-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 land.leets.domain.auth.AuthService;
-import land.leets.domain.shared.AuthRole;
-import land.leets.domain.user.domain.User;
-import land.leets.global.error.ErrorResponse;
-import land.leets.global.jwt.JwtProvider;
-import land.leets.global.jwt.dto.JwtResponse;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-
-@RestController
-@RequiredArgsConstructor
-public class AuthController {
-
- private final AuthService authService;
- private final JwtProvider jwtProvider;
-
- @Operation(summary = "(로그인) 유저 로그인", description = "구글 토큰으로 로그인합니다.")
- @ApiResponses({
- @ApiResponse(responseCode = "200"),
- @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
- @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
- @ApiResponse(responseCode = "500", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
- })
- @GetMapping("/login/oauth2/callback/google")
- public JwtResponse get(@RequestParam("code") String code) throws GeneralSecurityException, IOException {
- User user = authService.getGoogleToken(code);
- String accessToken = this.jwtProvider.generateToken(user.getId(), user.getEmail(), AuthRole.ROLE_USER, false);
- String refreshToken = this.jwtProvider.generateToken(user.getId(), user.getEmail(), AuthRole.ROLE_USER, true);
-
- return new JwtResponse(accessToken, refreshToken);
- }
-
- @GetMapping("/health-check")
- public ResponseEntity checkHealthStatus() {
- return new ResponseEntity<>(HttpStatus.OK);
- }
-}
diff --git a/src/main/java/land/leets/domain/auth/presentation/dto/OAuthTokenDto.java b/src/main/java/land/leets/domain/auth/presentation/dto/OAuthTokenDto.java
deleted file mode 100644
index 39bfc4b..0000000
--- a/src/main/java/land/leets/domain/auth/presentation/dto/OAuthTokenDto.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package land.leets.domain.auth.presentation.dto;
-
-import lombok.*;
-
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
-@AllArgsConstructor
-@Getter
-public class OAuthTokenDto {
- private String access_token;
- private String expires_in;
- private String scope;
- private String token_type;
- private String id_token;
-}
diff --git a/src/main/java/land/leets/domain/image/presentation/ImageController.java b/src/main/java/land/leets/domain/image/presentation/ImageController.java
deleted file mode 100644
index 9e6fc80..0000000
--- a/src/main/java/land/leets/domain/image/presentation/ImageController.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package land.leets.domain.image.presentation;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.core.io.Resource;
-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.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequiredArgsConstructor
-@RequestMapping("/images")
-public class ImageController {
-
- @Value("${image.path}")
- private String imageStoragePath;
-
- @GetMapping("/{imageName}")
- public ResponseEntity> getImage(@PathVariable String imageName) {
- Resource resource = new ClassPathResource(imageStoragePath + imageName);
- return new ResponseEntity<>(resource, HttpStatus.OK);
- }
-}
diff --git a/src/main/java/land/leets/domain/shared/BaseTimeEntity.java b/src/main/java/land/leets/domain/shared/BaseTimeEntity.java
deleted file mode 100644
index 3593d89..0000000
--- a/src/main/java/land/leets/domain/shared/BaseTimeEntity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package land.leets.domain.shared;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.EntityListeners;
-import jakarta.persistence.MappedSuperclass;
-import lombok.Getter;
-import org.springframework.data.annotation.CreatedDate;
-import org.springframework.data.annotation.LastModifiedDate;
-import org.springframework.data.jpa.domain.support.AuditingEntityListener;
-
-import java.time.LocalDateTime;
-
-@Getter
-@EntityListeners(AuditingEntityListener.class)
-@MappedSuperclass
-public class BaseTimeEntity {
-
- @CreatedDate
- @Column(updatable = false)
- private LocalDateTime createdAt;
-
- @LastModifiedDate
- @Column(updatable = false)
- private LocalDateTime updatedAt;
-
- public LocalDateTime getCreatedAt() {
- return createdAt;
- }
-
- public LocalDateTime getUpdatedAt() {
- return updatedAt;
- }
-}
diff --git a/src/main/java/land/leets/domain/shared/exception/PasswordNotMatchException.java b/src/main/java/land/leets/domain/shared/exception/PasswordNotMatchException.java
deleted file mode 100644
index d9e0e97..0000000
--- a/src/main/java/land/leets/domain/shared/exception/PasswordNotMatchException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package land.leets.domain.shared.exception;
-
-import land.leets.global.error.ErrorCode;
-import land.leets.global.error.exception.ServiceException;
-
-public class PasswordNotMatchException extends ServiceException {
- public PasswordNotMatchException() {
- super(ErrorCode.PASSWORD_NOT_MATCH);
- }
-}
diff --git a/src/main/java/land/leets/global/advice/ResponseAdvice.java b/src/main/java/land/leets/global/advice/ResponseAdvice.java
deleted file mode 100644
index 40671f9..0000000
--- a/src/main/java/land/leets/global/advice/ResponseAdvice.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package land.leets.global.advice;
-
-import org.springframework.core.MethodParameter;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.server.ServerHttpRequest;
-import org.springframework.http.server.ServerHttpResponse;
-import org.springframework.web.bind.annotation.RestControllerAdvice;
-import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
-
-import java.util.HashMap;
-import java.util.Map;
-
-@RestControllerAdvice
-public class ResponseAdvice implements ResponseBodyAdvice {
-
- private static final String API_DOCS_PATH = "/v3/api-docs";
- private static final String IMAGE_PATH = "/images";
-
- @Override
- public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
- return true;
- }
-
- @Override
- public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType,
- ServerHttpRequest req, ServerHttpResponse res) {
- if (req.getURI().getPath().contains(API_DOCS_PATH)) return body;
- if (req.getURI().getPath().contains(IMAGE_PATH)) return body;
-
- Map updatedResponse = new HashMap<>();
-
- updatedResponse.put("result", body);
- return updatedResponse;
- }
-}
diff --git a/src/main/java/land/leets/global/config/AsyncConfig.java b/src/main/java/land/leets/global/config/AsyncConfig.java
deleted file mode 100644
index 220fe2d..0000000
--- a/src/main/java/land/leets/global/config/AsyncConfig.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package land.leets.global.config;
-
-import java.util.concurrent.Executor;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.scheduling.annotation.EnableAsync;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
-
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-@EnableAsync
-@Configuration
-public class AsyncConfig {
-
- @Bean
- public Executor taskExecutor() {
- ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
- executor.setCorePoolSize(2);
- executor.setMaxPoolSize(2);
- executor.setQueueCapacity(200);
- executor.setThreadNamePrefix("Mailing-");
- executor.setKeepAliveSeconds(60);
- executor.setRejectedExecutionHandler(((r, asyncExecutor) -> log.warn("더 이상 요청을 처리할 수 없습니다.")));
- executor.initialize();
- return executor;
- }
-}
diff --git a/src/main/java/land/leets/global/config/DatabaseSetup.java b/src/main/java/land/leets/global/config/DatabaseSetup.java
deleted file mode 100644
index cdd1216..0000000
--- a/src/main/java/land/leets/global/config/DatabaseSetup.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package land.leets.global.config;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.boot.ApplicationArguments;
-import org.springframework.boot.ApplicationRunner;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.stereotype.Component;
-
-@Component
-@RequiredArgsConstructor
-public class DatabaseSetup implements ApplicationRunner {
-
- private final JdbcTemplate jdbcTemplate;
-
- @Override
- public void run(ApplicationArguments args) {
- jdbcTemplate.execute("ALTER DATABASE leets CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci");
- jdbcTemplate.execute("ALTER TABLE portfolios CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
- }
-}
diff --git a/src/main/java/land/leets/global/config/WebClientConfig.java b/src/main/java/land/leets/global/config/WebClientConfig.java
deleted file mode 100644
index de83bb8..0000000
--- a/src/main/java/land/leets/global/config/WebClientConfig.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package land.leets.global.config;
-
-import io.netty.channel.ChannelOption;
-import io.netty.handler.timeout.ReadTimeoutHandler;
-import io.netty.handler.timeout.WriteTimeoutHandler;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.http.client.reactive.ClientHttpConnector;
-import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-import org.springframework.web.reactive.function.client.WebClient;
-import reactor.netty.http.client.HttpClient;
-
-import java.time.Duration;
-
-@Configuration
-public class WebClientConfig {
-
- @Bean
- public WebClient webClient() {
- HttpClient httpClient = HttpClient.create()
- .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
- .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10))
- .addHandlerLast(new WriteTimeoutHandler(10)))
- .responseTimeout(Duration.ofSeconds(1));
-
- ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
- return WebClient.builder().clientConnector(connector).build();
- }
-}
diff --git a/src/main/java/land/leets/global/swagger/SwaggerConfig.java b/src/main/java/land/leets/global/swagger/SwaggerConfig.java
deleted file mode 100644
index 0146e0a..0000000
--- a/src/main/java/land/leets/global/swagger/SwaggerConfig.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package land.leets.global.swagger;
-
-import io.swagger.v3.oas.annotations.OpenAPIDefinition;
-import io.swagger.v3.oas.annotations.info.Info;
-import io.swagger.v3.oas.models.Components;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.security.SecurityRequirement;
-import io.swagger.v3.oas.models.security.SecurityScheme;
-import io.swagger.v3.oas.models.servers.Server;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@OpenAPIDefinition(info = @Info(
- title = "Leets API",
- description = "Leets API 문서",
- version = "v1.0.0"))
-@Configuration
-public class SwaggerConfig {
- @Bean
- public OpenAPI openApi() {
- String jwt = "JWT";
- SecurityRequirement securityRequirement = new SecurityRequirement().addList("JWT");
- Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme()
- .name(jwt)
- .type(SecurityScheme.Type.HTTP)
- .scheme("bearer")
- .bearerFormat("JWT")
- );
-
- return new OpenAPI()
- .addServersItem(new Server().url("/"))
- .addSecurityItem(securityRequirement)
- .components(components);
- }
-}
diff --git a/src/main/java/land/leets/LeetsApplication.java b/src/main/kotlin/land/leets/LeetsApplication.kt
similarity index 53%
rename from src/main/java/land/leets/LeetsApplication.java
rename to src/main/kotlin/land/leets/LeetsApplication.kt
index 818598e..577931e 100644
--- a/src/main/java/land/leets/LeetsApplication.java
+++ b/src/main/kotlin/land/leets/LeetsApplication.kt
@@ -1,17 +1,15 @@
-package land.leets;
+package land.leets
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
-import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing
+import org.springframework.scheduling.annotation.EnableScheduling
@SpringBootApplication
@EnableJpaAuditing
@EnableScheduling
-public class LeetsApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(LeetsApplication.class, args);
- }
+class LeetsApplication
+fun main(args: Array) {
+ runApplication(*args)
}
diff --git a/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt b/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt
index 2553ee9..8016f25 100644
--- a/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt
+++ b/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt
@@ -2,9 +2,8 @@ package land.leets.domain.admin.domain.repository
import land.leets.domain.admin.domain.Admin
import org.springframework.data.jpa.repository.JpaRepository
-import java.util.Optional
import java.util.UUID
interface AdminRepository : JpaRepository {
- fun findByUsername(username: String): Optional
-}
\ No newline at end of file
+ fun findByUsername(username: String): Admin?
+}
diff --git a/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt b/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt
index ad6adbf..bf83e25 100644
--- a/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt
+++ b/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt
@@ -17,7 +17,7 @@ class AdminLoginImpl(
) : AdminLogin {
override fun execute(username: String, password: String): JwtResponse {
- val admin = adminRepository.findByUsername(username).orElseThrow { AdminNotFoundException() }
+ val admin = adminRepository.findByUsername(username) ?: throw AdminNotFoundException()
if (!passwordEncoder.matches(password, admin.password)) {
throw PasswordNotMatchException()
diff --git a/src/main/kotlin/land/leets/domain/auth/AdminAuthDetailsService.kt b/src/main/kotlin/land/leets/domain/auth/AdminAuthDetailsService.kt
new file mode 100644
index 0000000..c8b6dda
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/AdminAuthDetailsService.kt
@@ -0,0 +1,21 @@
+package land.leets.domain.auth
+
+import land.leets.domain.admin.domain.repository.AdminRepository
+import land.leets.domain.shared.AuthRole
+import land.leets.global.error.ErrorCode
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Service
+
+@Service
+class AdminAuthDetailsService(
+ private val adminRepository: AdminRepository
+) : UserDetailsService {
+
+ override fun loadUserByUsername(sub: String): AuthDetails {
+ val admin = adminRepository.findByUsername(sub)
+ ?: throw UsernameNotFoundException(ErrorCode.ADMIN_NOT_FOUND.message)
+
+ return AuthDetails(admin.id!!, admin.username, AuthRole.ROLE_ADMIN)
+ }
+}
diff --git a/src/main/kotlin/land/leets/domain/auth/AuthDetails.kt b/src/main/kotlin/land/leets/domain/auth/AuthDetails.kt
new file mode 100644
index 0000000..9f5afd5
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/AuthDetails.kt
@@ -0,0 +1,30 @@
+package land.leets.domain.auth
+
+import land.leets.domain.shared.AuthRole
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.core.userdetails.UserDetails
+import java.util.UUID
+
+class AuthDetails(
+ val uid: UUID,
+ val email: String,
+ val role: AuthRole
+) : UserDetails {
+
+ override fun getAuthorities(): Collection {
+ return listOf(SimpleGrantedAuthority(role.role))
+ }
+
+ override fun getPassword(): String? = null
+
+ override fun getUsername(): String = email
+
+ override fun isAccountNonExpired(): Boolean = true
+
+ override fun isAccountNonLocked(): Boolean = true
+
+ override fun isCredentialsNonExpired(): Boolean = true
+
+ override fun isEnabled(): Boolean = true
+}
diff --git a/src/main/kotlin/land/leets/domain/auth/AuthService.kt b/src/main/kotlin/land/leets/domain/auth/AuthService.kt
new file mode 100644
index 0000000..5f3319a
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/AuthService.kt
@@ -0,0 +1,77 @@
+package land.leets.domain.auth
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
+import com.google.api.client.http.javanet.NetHttpTransport
+import com.google.api.client.json.gson.GsonFactory
+import io.github.oshai.kotlinlogging.KotlinLogging
+import land.leets.domain.auth.exception.PermissionDeniedException
+import land.leets.domain.auth.presentation.dto.OAuthTokenDto
+import land.leets.domain.user.domain.User
+import land.leets.domain.user.domain.repository.UserRepository
+import land.leets.global.jwt.exception.InvalidTokenException
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.http.HttpStatus
+import org.springframework.stereotype.Service
+import org.springframework.web.client.RestTemplate
+
+private val log = KotlinLogging.logger {}
+
+@Service
+class AuthService(
+ @Value("\${google.auth.url}") private val googleAuthUrl: String,
+ @Value("\${google.redirect.url}") private val googleRedirectUrl: String,
+ @Value("\${spring.security.oauth2.client.registration.google.client-id}") private val googleClientId: String,
+ @Value("\${spring.security.oauth2.client.registration.google.client-secret}") private val googleClientPassword: String,
+ private val userRepository: UserRepository,
+) {
+
+ fun getGoogleToken(code: String): User {
+ val restTemplate = RestTemplate()
+ val params = mapOf(
+ "code" to code,
+ "client_id" to googleClientId,
+ "client_secret" to googleClientPassword,
+ "redirect_uri" to googleRedirectUrl,
+ "grant_type" to "authorization_code"
+ )
+
+ val responseEntity = restTemplate.postForEntity(googleAuthUrl, params, OAuthTokenDto::class.java)
+
+ if (responseEntity.statusCode != HttpStatus.OK || responseEntity.body == null) {
+ throw PermissionDeniedException()
+ }
+
+ val idToken = responseEntity.body!!.idToken
+ return getUser(idToken)
+ }
+
+ fun getUser(idToken: String): User {
+ val verifier = GoogleIdTokenVerifier.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance())
+ .setAudience(listOf(googleClientId))
+ .build()
+
+ val googleIdToken = runCatching {
+ verifier.verify(idToken)
+ }.onFailure { e ->
+ log.debug {
+ "${"Google ID 토큰 인증 실패: {}"} ${
+ e::class.simpleName
+ }"
+ }
+ }.getOrNull() ?: throw InvalidTokenException()
+
+
+ val payload = googleIdToken.payload
+ val userId = payload.subject
+
+ return userRepository.findBySub(userId) ?: userRepository.save(
+ User(
+ sid = null,
+ name = payload["name"].toString(),
+ phone = null,
+ email = payload.email,
+ sub = userId
+ )
+ )
+ }
+}
diff --git a/src/main/kotlin/land/leets/domain/auth/UserAuthDetailsService.kt b/src/main/kotlin/land/leets/domain/auth/UserAuthDetailsService.kt
new file mode 100644
index 0000000..bfef56f
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/UserAuthDetailsService.kt
@@ -0,0 +1,21 @@
+package land.leets.domain.auth
+
+import land.leets.domain.shared.AuthRole
+import land.leets.domain.user.domain.repository.UserRepository
+import land.leets.global.error.ErrorCode
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Service
+
+@Service
+class UserAuthDetailsService(
+ private val userRepository: UserRepository
+) : UserDetailsService {
+
+ override fun loadUserByUsername(email: String): AuthDetails {
+ val user = userRepository.findByEmail(email)
+ ?: throw UsernameNotFoundException(ErrorCode.USER_NOT_FOUND.message)
+
+ return AuthDetails(user.id!!, user.email, AuthRole.ROLE_USER)
+ }
+}
diff --git a/src/main/kotlin/land/leets/domain/auth/exception/PermissionDeniedException.kt b/src/main/kotlin/land/leets/domain/auth/exception/PermissionDeniedException.kt
new file mode 100644
index 0000000..9cf1433
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/exception/PermissionDeniedException.kt
@@ -0,0 +1,6 @@
+package land.leets.domain.auth.exception
+
+import land.leets.global.error.ErrorCode
+import land.leets.global.error.exception.ServiceException
+
+class PermissionDeniedException : ServiceException(ErrorCode.PERMISSION_DENIED)
diff --git a/src/main/kotlin/land/leets/domain/auth/presentation/AuthController.kt b/src/main/kotlin/land/leets/domain/auth/presentation/AuthController.kt
new file mode 100644
index 0000000..db59255
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/presentation/AuthController.kt
@@ -0,0 +1,56 @@
+package land.leets.domain.auth.presentation
+
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.media.Content
+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 land.leets.domain.auth.AuthService
+import land.leets.domain.shared.AuthRole
+import land.leets.global.error.ErrorResponse
+import land.leets.global.jwt.JwtProvider
+import land.leets.global.jwt.dto.JwtResponse
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+class AuthController(
+ private val authService: AuthService,
+ private val jwtProvider: JwtProvider
+) {
+
+ @Operation(summary = "(로그인) 유저 로그인", description = "구글 토큰으로 로그인합니다.")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200"),
+ ApiResponse(
+ responseCode = "400",
+ content = [Content(schema = Schema(implementation = ErrorResponse::class))]
+ ),
+ ApiResponse(
+ responseCode = "404",
+ content = [Content(schema = Schema(implementation = ErrorResponse::class))]
+ ),
+ ApiResponse(
+ responseCode = "500",
+ content = [Content(schema = Schema(implementation = ErrorResponse::class))]
+ )
+ ]
+ )
+ @GetMapping("/login/oauth2/callback/google")
+ fun get(@RequestParam("code") code: String): JwtResponse {
+ val user = authService.getGoogleToken(code)
+ val accessToken = jwtProvider.generateToken(user.id!!, user.email, AuthRole.ROLE_USER, false)
+ val refreshToken = jwtProvider.generateToken(user.id, user.email, AuthRole.ROLE_USER, true)
+
+ return JwtResponse(accessToken, refreshToken)
+ }
+
+ @GetMapping("/health-check")
+ fun checkHealthStatus(): ResponseEntity {
+ return ResponseEntity(HttpStatus.OK)
+ }
+}
diff --git a/src/main/kotlin/land/leets/domain/auth/presentation/dto/OAuthTokenDto.kt b/src/main/kotlin/land/leets/domain/auth/presentation/dto/OAuthTokenDto.kt
new file mode 100644
index 0000000..a289c74
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/auth/presentation/dto/OAuthTokenDto.kt
@@ -0,0 +1,11 @@
+package land.leets.domain.auth.presentation.dto
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class OAuthTokenDto(
+ @JsonProperty("access_token") val accessToken: String,
+ @JsonProperty("expires_in") val expiresIn: String,
+ val scope: String,
+ @JsonProperty("token_type") val tokenType: String,
+ @JsonProperty("id_token") val idToken: String
+)
diff --git a/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt b/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt
index 72607e2..f4ed25e 100644
--- a/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt
+++ b/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt
@@ -7,9 +7,9 @@ import java.time.LocalDateTime
data class CommentResponse(
val content: String,
- val createdAt: LocalDateTime,
+ val createdAt: LocalDateTime?,
- val updatedAt: LocalDateTime,
+ val updatedAt: LocalDateTime?,
val admin: AdminDetailsResponse
) {
diff --git a/src/main/kotlin/land/leets/domain/image/presentation/ImageController.kt b/src/main/kotlin/land/leets/domain/image/presentation/ImageController.kt
new file mode 100644
index 0000000..7f98d6d
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/image/presentation/ImageController.kt
@@ -0,0 +1,24 @@
+package land.leets.domain.image.presentation
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.core.io.ClassPathResource
+import org.springframework.core.io.Resource
+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.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/images")
+class ImageController(
+ @Value("\${image.path}") private val imageStoragePath: String
+) {
+
+ @GetMapping("/{imageName}")
+ fun getImage(@PathVariable imageName: String): ResponseEntity {
+ val resource = ClassPathResource(imageStoragePath + imageName)
+ return ResponseEntity(resource, HttpStatus.OK)
+ }
+}
diff --git a/src/main/kotlin/land/leets/domain/shared/BaseTimeEntity.kt b/src/main/kotlin/land/leets/domain/shared/BaseTimeEntity.kt
new file mode 100644
index 0000000..ab5ee20
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/shared/BaseTimeEntity.kt
@@ -0,0 +1,22 @@
+package land.leets.domain.shared
+
+import jakarta.persistence.Column
+import jakarta.persistence.EntityListeners
+import jakarta.persistence.MappedSuperclass
+import org.springframework.data.annotation.CreatedDate
+import org.springframework.data.annotation.LastModifiedDate
+import org.springframework.data.jpa.domain.support.AuditingEntityListener
+import java.time.LocalDateTime
+
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener::class)
+abstract class BaseTimeEntity {
+
+ @CreatedDate
+ @Column(updatable = false)
+ var createdAt: LocalDateTime? = null
+
+ @LastModifiedDate
+ @Column
+ var updatedAt: LocalDateTime? = null
+}
diff --git a/src/main/kotlin/land/leets/domain/shared/exception/PasswordNotMatchException.kt b/src/main/kotlin/land/leets/domain/shared/exception/PasswordNotMatchException.kt
new file mode 100644
index 0000000..bbe703b
--- /dev/null
+++ b/src/main/kotlin/land/leets/domain/shared/exception/PasswordNotMatchException.kt
@@ -0,0 +1,6 @@
+package land.leets.domain.shared.exception
+
+import land.leets.global.error.ErrorCode
+import land.leets.global.error.exception.ServiceException
+
+class PasswordNotMatchException : ServiceException(ErrorCode.PASSWORD_NOT_MATCH)
diff --git a/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt b/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt
index d14f84b..952dc88 100644
--- a/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt
+++ b/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt
@@ -2,10 +2,9 @@ package land.leets.domain.user.domain.repository
import land.leets.domain.user.domain.User
import org.springframework.data.jpa.repository.JpaRepository
-import java.util.Optional
import java.util.UUID
interface UserRepository : JpaRepository {
- fun findBySub(sub: String): Optional
- fun findByEmail(email: String): Optional
+ fun findBySub(sub: String): User?
+ fun findByEmail(email: String): User?
}
diff --git a/src/main/kotlin/land/leets/global/advise/ExceptionHandleAdvice.kt b/src/main/kotlin/land/leets/global/advice/ExceptionHandleAdvice.kt
similarity index 98%
rename from src/main/kotlin/land/leets/global/advise/ExceptionHandleAdvice.kt
rename to src/main/kotlin/land/leets/global/advice/ExceptionHandleAdvice.kt
index 77a758a..bb69514 100644
--- a/src/main/kotlin/land/leets/global/advise/ExceptionHandleAdvice.kt
+++ b/src/main/kotlin/land/leets/global/advice/ExceptionHandleAdvice.kt
@@ -1,4 +1,4 @@
-package land.leets.global.advise
+package land.leets.global.advice
import land.leets.global.error.ErrorCode
import land.leets.global.error.ErrorResponse
diff --git a/src/main/kotlin/land/leets/global/advice/ResponseAdvice.kt b/src/main/kotlin/land/leets/global/advice/ResponseAdvice.kt
new file mode 100644
index 0000000..147b6d4
--- /dev/null
+++ b/src/main/kotlin/land/leets/global/advice/ResponseAdvice.kt
@@ -0,0 +1,41 @@
+package land.leets.global.advice
+
+import org.springframework.core.MethodParameter
+import org.springframework.http.MediaType
+import org.springframework.http.converter.HttpMessageConverter
+import org.springframework.http.server.ServerHttpRequest
+import org.springframework.http.server.ServerHttpResponse
+import org.springframework.web.bind.annotation.RestControllerAdvice
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
+
+@RestControllerAdvice
+class ResponseAdvice : ResponseBodyAdvice {
+
+ companion object {
+ private const val API_DOCS_PATH = "/v3/api-docs"
+ private const val IMAGE_PATH = "/images"
+ }
+
+ override fun supports(
+ returnType: MethodParameter,
+ converterType: Class>
+ ): Boolean {
+ return true
+ }
+
+ override fun beforeBodyWrite(
+ body: Any?,
+ returnType: MethodParameter,
+ selectedContentType: MediaType,
+ selectedConverterType: Class>,
+ req: ServerHttpRequest,
+ res: ServerHttpResponse
+ ): Any? {
+ if (req.uri.path.contains(API_DOCS_PATH)) return body
+ if (req.uri.path.contains(IMAGE_PATH)) return body
+
+ val updatedResponse = mutableMapOf()
+ updatedResponse["result"] = body
+ return updatedResponse
+ }
+}
diff --git a/src/main/kotlin/land/leets/global/config/DatabaseSetup.kt b/src/main/kotlin/land/leets/global/config/DatabaseSetup.kt
new file mode 100644
index 0000000..8d8ca55
--- /dev/null
+++ b/src/main/kotlin/land/leets/global/config/DatabaseSetup.kt
@@ -0,0 +1,17 @@
+package land.leets.global.config
+
+import org.springframework.boot.ApplicationArguments
+import org.springframework.boot.ApplicationRunner
+import org.springframework.jdbc.core.JdbcTemplate
+import org.springframework.stereotype.Component
+
+@Component
+class DatabaseSetup(
+ private val jdbcTemplate: JdbcTemplate
+) : ApplicationRunner {
+
+ override fun run(args: ApplicationArguments) {
+ jdbcTemplate.execute("ALTER DATABASE leets CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci")
+ jdbcTemplate.execute("ALTER TABLE portfolios CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
+ }
+}
diff --git a/src/main/kotlin/land/leets/global/config/WebClientConfig.kt b/src/main/kotlin/land/leets/global/config/WebClientConfig.kt
new file mode 100644
index 0000000..f386b61
--- /dev/null
+++ b/src/main/kotlin/land/leets/global/config/WebClientConfig.kt
@@ -0,0 +1,29 @@
+package land.leets.global.config
+
+import io.netty.channel.ChannelOption
+import io.netty.handler.timeout.ReadTimeoutHandler
+import io.netty.handler.timeout.WriteTimeoutHandler
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.client.reactive.ReactorClientHttpConnector
+import org.springframework.web.reactive.function.client.WebClient
+import reactor.netty.http.client.HttpClient
+import java.time.Duration
+
+@Configuration
+class WebClientConfig {
+
+ @Bean
+ fun webClient(): WebClient {
+ val httpClient = HttpClient.create()
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
+ .doOnConnected { connection ->
+ connection.addHandlerLast(ReadTimeoutHandler(10))
+ .addHandlerLast(WriteTimeoutHandler(10))
+ }
+ .responseTimeout(Duration.ofSeconds(1))
+
+ val connector = ReactorClientHttpConnector(httpClient)
+ return WebClient.builder().clientConnector(connector).build()
+ }
+}
diff --git a/src/main/kotlin/land/leets/global/swagger/SwaggerConfig.kt b/src/main/kotlin/land/leets/global/swagger/SwaggerConfig.kt
new file mode 100644
index 0000000..cfae7d8
--- /dev/null
+++ b/src/main/kotlin/land/leets/global/swagger/SwaggerConfig.kt
@@ -0,0 +1,41 @@
+package land.leets.global.swagger
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition
+import io.swagger.v3.oas.annotations.info.Info
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.security.SecurityRequirement
+import io.swagger.v3.oas.models.security.SecurityScheme
+import io.swagger.v3.oas.models.servers.Server
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@OpenAPIDefinition(
+ info = Info(
+ title = "Leets API",
+ description = "leets.land API 문서",
+ version = "v2.0.0"
+ )
+)
+@Configuration
+class SwaggerConfig {
+
+ @Bean
+ fun openApi(): OpenAPI {
+ val jwt = "JWT"
+ val securityRequirement = SecurityRequirement().addList("JWT")
+ val components = Components().addSecuritySchemes(
+ jwt,
+ SecurityScheme()
+ .name(jwt)
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ )
+
+ return OpenAPI()
+ .addServersItem(Server().url("/"))
+ .addSecurityItem(securityRequirement)
+ .components(components)
+ }
+}
diff --git a/src/test/java/land/leets/LeetsApplicationTests.java b/src/test/java/land/leets/LeetsApplicationTests.java
deleted file mode 100644
index 1fc9bea..0000000
--- a/src/test/java/land/leets/LeetsApplicationTests.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package land.leets;
-
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class LeetsApplicationTests {
-}
diff --git a/src/test/kotlin/land/leets/LeetsApplicationTests.kt b/src/test/kotlin/land/leets/LeetsApplicationTests.kt
new file mode 100644
index 0000000..3ff3a85
--- /dev/null
+++ b/src/test/kotlin/land/leets/LeetsApplicationTests.kt
@@ -0,0 +1,7 @@
+package land.leets
+
+import org.springframework.boot.test.context.SpringBootTest
+
+@SpringBootTest
+class LeetsApplicationTests {
+}
diff --git a/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt b/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt
index 973a738..f64f061 100644
--- a/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt
+++ b/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt
@@ -12,7 +12,6 @@ import land.leets.domain.shared.AuthRole
import land.leets.domain.shared.exception.PasswordNotMatchException
import land.leets.global.jwt.JwtProvider
import org.springframework.security.crypto.password.PasswordEncoder
-import java.util.Optional
import java.util.UUID
class AdminLoginImplTest : DescribeSpec({
@@ -31,7 +30,7 @@ class AdminLoginImplTest : DescribeSpec({
val admin = Admin(username, encodedPassword, "name", "email", id)
it("로그인이 성공하면 JwtResponse를 반환한다") {
- every { adminRepository.findByUsername(username) } returns Optional.of(admin)
+ every { adminRepository.findByUsername(username) } returns admin
every { passwordEncoder.matches(password, encodedPassword) } returns true
every { jwtProvider.generateToken(id, username, AuthRole.ROLE_ADMIN, false) } returns "accessToken"
every { jwtProvider.generateToken(id, username, AuthRole.ROLE_ADMIN, true) } returns "refreshToken"
@@ -43,7 +42,7 @@ class AdminLoginImplTest : DescribeSpec({
}
it("관리자를 찾을 수 없으면 AdminNotFoundException을 던진다") {
- every { adminRepository.findByUsername(username) } returns Optional.empty()
+ every { adminRepository.findByUsername(username) } returns null
shouldThrow {
adminLogin.execute(username, password)
@@ -51,7 +50,7 @@ class AdminLoginImplTest : DescribeSpec({
}
it("비밀번호가 일치하지 않으면 PasswordNotMatchException을 던진다") {
- every { adminRepository.findByUsername(username) } returns Optional.of(admin)
+ every { adminRepository.findByUsername(username) } returns admin
every { passwordEncoder.matches(password, encodedPassword) } returns false
shouldThrow {
diff --git a/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt b/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt
index e6dc297..86665b1 100644
--- a/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt
+++ b/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt
@@ -13,6 +13,7 @@ import land.leets.domain.application.presentation.dto.ApplicationRequest
import land.leets.domain.application.type.Position
import land.leets.domain.application.type.SubmitStatus
import land.leets.domain.auth.AuthDetails
+import land.leets.domain.shared.AuthRole
import land.leets.domain.user.domain.User
import land.leets.domain.user.domain.repository.UserRepository
import java.util.*
@@ -26,9 +27,7 @@ class CreateApplicationImplTest : DescribeSpec({
describe("CreateApplicationImpl 유스케이스는") {
context("지원서 생성을 요청할 때") {
val uid = UUID.randomUUID()
- val authDetails = mockk {
- every { getUid() } returns uid
- }
+ val authDetails = AuthDetails(uid, "test@test.com", AuthRole.ROLE_USER)
val request = ApplicationRequest(
name = "Test",
sid = "20202020",
diff --git a/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt b/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt
index 3d10d31..bebb964 100644
--- a/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt
+++ b/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt
@@ -10,6 +10,7 @@ import land.leets.domain.application.presentation.dto.ApplicationRequest
import land.leets.domain.application.type.Position
import land.leets.domain.application.type.SubmitStatus
import land.leets.domain.auth.AuthDetails
+import land.leets.domain.shared.AuthRole
import land.leets.domain.user.usecase.UpdateUser
import java.util.*
@@ -22,9 +23,7 @@ class UpdateApplicationImplTest : DescribeSpec({
describe("UpdateApplicationImpl 유스케이스는") {
context("지원서 수정을 요청할 때") {
val uid = UUID.randomUUID()
- val authDetails = mockk {
- every { getUid() } returns uid
- }
+ val authDetails = AuthDetails(uid, "test@test.com", AuthRole.ROLE_USER)
val request = ApplicationRequest(
name = "Test Updated",
sid = "20202020",