Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94b5140
feat : 스프링 시큐리티 적용&jwt구현
HyemIin Jun 2, 2025
6488326
refactor : getter 적용 및 final 키워드 적용
HyemIin Jun 2, 2025
731c4a1
refactor : 미사용 예외 제거
HyemIin Jun 2, 2025
da7f0da
Merge pull request #12 from GTable/feature/#8-store-crud
Jjiggu Jun 2, 2025
54efc75
refactor(KakaoLogin): 카카오로그인_OAuth 코드 수정
HyemIin Jun 3, 2025
4b81dba
Merge branch 'develop' into #10-카카오로그인구현
HyemIin Jun 3, 2025
d890665
Merge pull request #13 from GTable/#10-카카오로그인구현
HyemIin Jun 3, 2025
a7e60d0
feat(config): add AsyncConfig with s3UploadExecutor thread pool
Jjiggu Jun 4, 2025
71155f8
feat(config): add AwsS3Config for AwsS3Client bean configuration
Jjiggu Jun 4, 2025
68a5c2b
chore(build): add AWS S3 and Resilience4j depende
Jjiggu Jun 4, 2025
27a8dad
feat(s3): implements S3Service with async upload and delete method
Jjiggu Jun 4, 2025
7624cb6
chore(security): permit all requests to /stores/**
Jjiggu Jun 4, 2025
57d554c
feat(store-images): add StoreImage entity
Jjiggu Jun 4, 2025
cfb4cfb
feat(store-images): add endpoints for uploading and deleting store im…
Jjiggu Jun 4, 2025
95c2b71
feat(store-images): add StoreImageRepository
Jjiggu Jun 4, 2025
eb1ee68
feat(store-images): add StoreImageService for batch upload and delete…
Jjiggu Jun 4, 2025
bf8a1de
feat(store-images): add StoreImageUploadResponse dto with fromEntity …
Jjiggu Jun 4, 2025
2be16e4
refactor(store): remove setters, imageUrl, and add update methods
Jjiggu Jun 4, 2025
05f9bb6
refactor(store): remove imageUrl field
Jjiggu Jun 4, 2025
efa11ea
refactor(store): remove imageUrl, add List<StoreImageUploadResponse>
Jjiggu Jun 4, 2025
c2f4ffb
refactor(store): rename fromEntity to of in StoreReadResponse
Jjiggu Jun 4, 2025
55a5b5d
feat(store): include store image information in StoreReadDto mapping
Jjiggu Jun 4, 2025
455f6bf
refactor(store): remove imageUrl
Jjiggu Jun 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,46 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Web MVC
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'


// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// TEST
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// OAUTH2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
// SPRING SECURITY
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.683'
// Resilience4j
implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
implementation 'io.github.resilience4j:resilience4j-bulkhead:1.7.1'
// 비동기 실행
implementation 'org.springframework.boot:spring-boot-starter-aop'

testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.awaitility:awaitility:4.3.0'
testImplementation 'com.h2database:h2'


}

tasks.named('test') {
Expand Down
27 changes: 14 additions & 13 deletions src/main/java/com/example/gtable/global/api/ApiError.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
package com.example.gtable.global.api;

import lombok.Getter;
import org.springframework.http.HttpStatus;

import lombok.Getter;

@Getter
public class ApiError {
private final String message;
private final int status;
private final String message;
private final int status;

public ApiError(String message, int status) {
this.message = message;
this.status = status;
}
public ApiError(String message, int status) {
this.message = message;
this.status = status;
}

public ApiError(Throwable throwable, HttpStatus status) {
this(throwable.getMessage(), status);
}
public ApiError(Throwable throwable, HttpStatus status) {
this(throwable.getMessage(), status);
}

public ApiError(String message, HttpStatus status) {
this(message, status.value());
}
public ApiError(String message, HttpStatus status) {
this(message, status.value());
}
}
28 changes: 14 additions & 14 deletions src/main/java/com/example/gtable/global/api/ApiResult.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package com.example.gtable.global.api;

public class ApiResult<T> {
private final boolean success;
private final T response;
private final ApiError error;
private final boolean success;
private final T response;
private final ApiError error;

public ApiResult(boolean success, T response, ApiError error) {
this.success = success;
this.response = response;
this.error = error;
}
public ApiResult(boolean success, T response, ApiError error) {
this.success = success;
this.response = response;
this.error = error;
}

public boolean isSuccess() {
return success;
}
public boolean isSuccess() {
return success;
}

public T getResponse() {
return response;
}
public T getResponse() {
return response;
}
}
21 changes: 11 additions & 10 deletions src/main/java/com/example/gtable/global/api/ApiUtils.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.example.gtable.global.api;

import org.springframework.http.HttpStatus;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiUtils {
public static <T> ApiResult<T> success(T response) {
return new ApiResult<>(true, response,null);
}
public static <T> ApiResult<T> success(T response) {
return new ApiResult<>(true, response, null);
}

public static ApiResult<?> error(Throwable throwable, HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(throwable, status));
}
public static ApiResult<?> error(Throwable throwable, HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(throwable, status));
}

public static ApiResult<?> error(String message,HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(message,status));
}
public static ApiResult<?> error(String message, HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(message, status));
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/example/gtable/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.gtable.global.config;

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class AsyncConfig {
@Bean(name = "s3UploadExecutor")
public Executor s3UploadExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("S3Upload-");
executor.initialize();
return executor;
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/example/gtable/global/config/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.gtable.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

@Configuration
public class AwsS3Config {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client)AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}

}
28 changes: 28 additions & 0 deletions src/main/java/com/example/gtable/global/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.gtable.global.config;

import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();

config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더
config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정
// config.setAllowCredentials(true); // 쿠키 포함 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미
return source;
}
}
65 changes: 65 additions & 0 deletions src/main/java/com/example/gtable/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.example.gtable.global.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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;

import com.example.gtable.global.security.jwt.JwtAuthorizationFilter;
import com.example.gtable.global.security.jwt.JwtUtil;
import com.example.gtable.global.security.oauth2.CustomOAuth2UserService;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity // security 활성화 어노테이션
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final com.example.gtable.global.security.oauth2.OAuth2LoginSuccessHandler OAuth2LoginSuccessHandler;
private final JwtUtil jwtUtil;

private final CorsConfigurationSource corsConfigurationSource;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF 방어 기능 비활성화 (jwt 토큰을 사용할 것이기에 필요없음)
.csrf(AbstractHttpConfigurer::disable)
// 시큐리티 폼 로그인 비활성화
.formLogin(AbstractHttpConfigurer::disable)
// HTTP Basic 인증 비활성화
.httpBasic(AbstractHttpConfigurer::disable)
// oauth2 로그인
// - userInfoEndPoint에서 사용자 정보 불러오고,
// - successHandler에서 로그인 성공 시 JWT 생성 및 반환로직
.oauth2Login(oauth2 ->
oauth2.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.userService(customOAuth2UserService)
).successHandler(OAuth2LoginSuccessHandler)
)
// 세션 사용하지 않음
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/oauth2/authorization/kakao", // 카카오 로그인 요청
"/login/oauth2/code/**", // 카카오 인증 콜백
"/api/refresh-token",
"/stores/**") // refresh token (토큰 갱신)
.permitAll()
.anyRequest().authenticated() // 그외 요청은 허가된 사람만 인가
)
// JWTFiler
.addFilterBefore(new JwtAuthorizationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

}
57 changes: 57 additions & 0 deletions src/main/java/com/example/gtable/global/s3/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.example.gtable.global.s3;

import java.io.InputStream;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.ObjectMetadata;

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3Client amazonS3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

public record S3UploadResult(String key, String url) {
}

@Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL)
@Async("s3UploadExecutor")
public CompletableFuture<S3UploadResult> upload(Long storeId, MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
String key = createFileKey(storeId, file.getOriginalFilename());
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());

amazonS3Client.putObject(bucket, key, inputStream, metadata);
String url = amazonS3Client.getUrl(bucket, key).toString();

return CompletableFuture.completedFuture(new S3UploadResult(key, url));
} catch (Exception e) {
throw new RuntimeException("S3 업로드 실패", e);
}
}

public void delete(String filename) {
try {
amazonS3Client.deleteObject(bucket, filename);
} catch (Exception e) {
throw new RuntimeException("S3 파일 삭제 실패", e);
}
}

private String createFileKey(Long storeId, String filename) {
return "store/" + storeId + "/" + UUID.randomUUID() + "-" + filename;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.gtable.global.security.exception;

public abstract class BusinessException extends RuntimeException {
private final ErrorMessage errorMessage;

protected BusinessException(ErrorMessage errorMessage) {
super(errorMessage.getMessage());
this.errorMessage = errorMessage;
}

public String getCode() {
return errorMessage.getCode();
}
}
Loading
Loading