This commit is contained in:
ondal
2025-02-12 21:24:01 +09:00
commit 7a4f60c842
222 changed files with 3018 additions and 0 deletions
@@ -0,0 +1,11 @@
package com.unicorn.lifesub.mysub.infra;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MySubApplication {
public static void main(String[] args) {
SpringApplication.run(MySubApplication.class, args);
}
}
@@ -0,0 +1,55 @@
package com.unicorn.lifesub.mysub.infra.adapter;
import com.unicorn.lifesub.common.exception.BusinessException;
import com.unicorn.lifesub.common.exception.ErrorCode;
import com.unicorn.lifesub.mysub.biz.domain.MySubscription;
import com.unicorn.lifesub.mysub.biz.usecase.out.MySubscriptionReader;
import com.unicorn.lifesub.mysub.biz.usecase.out.MySubscriptionWriter;
import com.unicorn.lifesub.mysub.infra.entity.MySubscriptionEntity;
import com.unicorn.lifesub.mysub.infra.entity.SubscriptionEntity;
import com.unicorn.lifesub.mysub.infra.repository.MySubscriptionJpaRepository;
import com.unicorn.lifesub.mysub.infra.repository.SubscriptionJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class MySubscriptionAdapter implements MySubscriptionReader, MySubscriptionWriter {
private final MySubscriptionJpaRepository mySubscriptionRepository;
private final SubscriptionJpaRepository subscriptionRepository;
@Override
public List<MySubscription> findByUserId(String userId) {
return mySubscriptionRepository.findByUserId(userId).stream()
.map(MySubscriptionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Optional<MySubscription> findById(Long id) {
return mySubscriptionRepository.findById(id)
.map(MySubscriptionEntity::toDomain);
}
@Override
public MySubscription save(String userId, Long subscriptionId) {
SubscriptionEntity subscription = subscriptionRepository.findById(subscriptionId)
.orElseThrow(() -> new BusinessException(ErrorCode.SUBSCRIPTION_NOT_FOUND));
MySubscriptionEntity entity = MySubscriptionEntity.builder()
.userId(userId)
.subscription(subscription)
.build();
return mySubscriptionRepository.save(entity).toDomain();
}
@Override
public void delete(Long id) {
mySubscriptionRepository.deleteById(id);
}
}
@@ -0,0 +1,44 @@
package com.unicorn.lifesub.mysub.infra.adapter;
import com.unicorn.lifesub.common.exception.BusinessException;
import com.unicorn.lifesub.common.exception.ErrorCode;
import com.unicorn.lifesub.mysub.biz.domain.Subscription;
import com.unicorn.lifesub.mysub.biz.domain.Category;
import com.unicorn.lifesub.mysub.biz.usecase.out.SubscriptionReader;
import com.unicorn.lifesub.mysub.infra.entity.SubscriptionEntity;
import com.unicorn.lifesub.mysub.infra.entity.CategoryEntity;
import com.unicorn.lifesub.mysub.infra.repository.SubscriptionJpaRepository;
import com.unicorn.lifesub.mysub.infra.repository.CategoryJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class SubscriptionAdapter implements SubscriptionReader {
private final SubscriptionJpaRepository subscriptionRepository;
private final CategoryJpaRepository categoryRepository;
@Override
public Optional<Subscription> findById(Long id) {
return subscriptionRepository.findById(id)
.map(SubscriptionEntity::toDomain);
}
@Override
public List<Subscription> findByCategory(String categoryId) {
return subscriptionRepository.findByCategory(categoryId).stream()
.map(SubscriptionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Category> findAllCategories() {
return categoryRepository.findAll().stream()
.map(CategoryEntity::toDomain)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,65 @@
package com.unicorn.lifesub.mysub.infra.config;
import com.unicorn.lifesub.mysub.infra.config.jwt.JwtAuthenticationFilter;
import com.unicorn.lifesub.mysub.infra.config.jwt.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Value;
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.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@SuppressWarnings("unused")
public class SecurityConfig {
protected final JwtTokenProvider jwtTokenProvider;
@Value("${allowedorigins}")
private String allowedOrigins;
public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors
.configurationSource(corsConfigurationSource())
)
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs.yaml", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@@ -0,0 +1,28 @@
package com.unicorn.lifesub.mysub.infra.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@SuppressWarnings("unused")
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("마이구독 서비스 API")
.version("v1.0.0")
.description("마이구독 서비스 API 명세서입니다."));
}
}
@@ -0,0 +1,42 @@
// CommonJwtAuthenticationFilter.java
package com.unicorn.lifesub.mysub.infra.config.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
SecurityContextHolder.getContext().setAuthentication(jwtTokenProvider.getAuthentication(token));
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@@ -0,0 +1,58 @@
// CommonJwtTokenProvider.java
package com.unicorn.lifesub.mysub.infra.config.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.unicorn.lifesub.common.exception.ErrorCode;
import com.unicorn.lifesub.common.exception.InfraException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@Component
public class JwtTokenProvider {
private final Algorithm algorithm;
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
this.algorithm = Algorithm.HMAC512(secretKey);
}
public Authentication getAuthentication(String token) {
try {
DecodedJWT decodedJWT = JWT.decode(token);
String username = decodedJWT.getSubject();
String[] authStrings = decodedJWT.getClaim("auth").asArray(String.class);
Collection<? extends GrantedAuthority> authorities = Arrays.stream(authStrings)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails userDetails = new User(username, "", authorities);
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
} catch (Exception e) {
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
}
}
public boolean validateToken(String token) {
try {
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
return false;
}
}
}
@@ -0,0 +1,37 @@
package com.unicorn.lifesub.mysub.infra.controller;
import com.unicorn.lifesub.common.dto.ApiResponse;
import com.unicorn.lifesub.mysub.biz.dto.CategoryResponse;
import com.unicorn.lifesub.mysub.biz.dto.ServiceListResponse;
import com.unicorn.lifesub.mysub.biz.usecase.in.CategoryUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "구독 카테고리 API", description = "구독 카테고리 관련 API")
@RestController
@RequestMapping("/api/mysub")
@RequiredArgsConstructor
public class CategoryController {
private final CategoryUseCase categoryUseCase;
@Operation(summary = "전체 카테고리 목록 조회")
@GetMapping("/categories")
public ResponseEntity<ApiResponse<List<CategoryResponse>>> getAllCategories() {
return ResponseEntity.ok(ApiResponse.success(categoryUseCase.getAllCategories()));
}
@Operation(summary = "카테고리별 서비스 목록 조회")
@GetMapping("/services")
public ResponseEntity<ApiResponse<List<ServiceListResponse>>> getServicesByCategory(
@RequestParam String categoryId) {
return ResponseEntity.ok(ApiResponse.success(categoryUseCase.getServicesByCategory(categoryId)));
}
}
@@ -0,0 +1,37 @@
package com.unicorn.lifesub.mysub.infra.controller;
import com.unicorn.lifesub.common.dto.ApiResponse;
import com.unicorn.lifesub.mysub.biz.dto.MySubResponse;
import com.unicorn.lifesub.mysub.biz.dto.TotalFeeResponse;
import com.unicorn.lifesub.mysub.biz.usecase.in.MySubscriptionsUseCase;
import com.unicorn.lifesub.mysub.biz.usecase.in.TotalFeeUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "마이구독 API", description = "마이구독 관련 API")
@RestController
@RequestMapping("/api/mysub")
@RequiredArgsConstructor
public class MySubController {
private final TotalFeeUseCase totalFeeUseCase;
private final MySubscriptionsUseCase mySubscriptionsUseCase;
@Operation(summary = "총 구독료 조회", description = "사용자의 총 구독료를 조회합니다.")
@GetMapping("/total-fee")
public ResponseEntity<ApiResponse<TotalFeeResponse>> getTotalFee(@RequestParam String userId) {
TotalFeeResponse response = totalFeeUseCase.getTotalFee(userId);
return ResponseEntity.ok(ApiResponse.success(response));
}
@Operation(summary = "구독 목록 조회", description = "사용자의 구독 서비스 목록을 조회합니다.")
@GetMapping("/list")
public ResponseEntity<ApiResponse<List<MySubResponse>>> getMySubscriptions(@RequestParam String userId) {
List<MySubResponse> response = mySubscriptionsUseCase.getMySubscriptions(userId);
return ResponseEntity.ok(ApiResponse.success(response));
}
}
@@ -0,0 +1,47 @@
package com.unicorn.lifesub.mysub.infra.controller;
import com.unicorn.lifesub.common.dto.ApiResponse;
import com.unicorn.lifesub.mysub.biz.dto.SubDetailResponse;
import com.unicorn.lifesub.mysub.biz.usecase.in.CancelSubscriptionUseCase;
import com.unicorn.lifesub.mysub.biz.usecase.in.SubscribeUseCase;
import com.unicorn.lifesub.mysub.biz.usecase.in.SubscriptionDetailUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Tag(name = "구독 서비스 API", description = "구독 서비스 관련 API")
@RestController
@RequestMapping("/api/mysub/services")
@RequiredArgsConstructor
public class ServiceController {
private final SubscriptionDetailUseCase subscriptionDetailUseCase;
private final SubscribeUseCase subscribeUseCase;
private final CancelSubscriptionUseCase cancelSubscriptionUseCase;
@Operation(summary = "구독 서비스 상세 조회")
@GetMapping("/{subscriptionId}")
public ResponseEntity<ApiResponse<SubDetailResponse>> getSubscriptionDetail(
@PathVariable Long subscriptionId) {
return ResponseEntity.ok(ApiResponse.success(
subscriptionDetailUseCase.getSubscriptionDetail(subscriptionId)));
}
@Operation(summary = "구독 신청")
@PostMapping("/{subscriptionId}/subscribe")
public ResponseEntity<ApiResponse<Void>> subscribe(
@PathVariable Long subscriptionId,
@RequestParam String userId) {
subscribeUseCase.subscribe(subscriptionId, userId);
return ResponseEntity.ok(ApiResponse.success(null));
}
@Operation(summary = "구독 취소")
@DeleteMapping("/{subscriptionId}")
public ResponseEntity<ApiResponse<Void>> cancel(
@PathVariable Long subscriptionId) {
cancelSubscriptionUseCase.cancel(subscriptionId);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
@@ -0,0 +1,33 @@
package com.unicorn.lifesub.mysub.infra.entity;
import com.unicorn.lifesub.common.entity.BaseTimeEntity;
import com.unicorn.lifesub.mysub.biz.domain.Category;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
@Entity
@Table(name = "categories")
@Getter
@NoArgsConstructor
public class CategoryEntity extends BaseTimeEntity {
@Id
private String categoryId;
private String name;
@Builder
public CategoryEntity(String categoryId, String name) {
this.categoryId = categoryId;
this.name = name;
}
public Category toDomain() {
return Category.builder()
.categoryId(categoryId)
.name(name)
.build();
}
}
@@ -0,0 +1,38 @@
package com.unicorn.lifesub.mysub.infra.entity;
import com.unicorn.lifesub.mysub.biz.domain.MySubscription;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "my_subscriptions")
@Getter
@NoArgsConstructor
public class MySubscriptionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String userId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "subscription_id")
private SubscriptionEntity subscription;
@Builder
public MySubscriptionEntity(Long id, String userId, SubscriptionEntity subscription) {
this.id = id;
this.userId = userId;
this.subscription = subscription;
}
public MySubscription toDomain() {
return MySubscription.builder()
.id(id)
.userId(userId)
.subscription(subscription.toDomain())
.build();
}
}
@@ -0,0 +1,49 @@
package com.unicorn.lifesub.mysub.infra.entity;
import com.unicorn.lifesub.common.entity.BaseTimeEntity;
import com.unicorn.lifesub.mysub.biz.domain.Subscription;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "subscriptions")
@Getter
@NoArgsConstructor
public class SubscriptionEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private String category;
private int price;
private int maxSharedUsers;
private String logoUrl;
@Builder
public SubscriptionEntity(Long id, String name, String description, String category,
int price, int maxSharedUsers, String logoUrl) {
this.id = id;
this.name = name;
this.description = description;
this.category = category;
this.price = price;
this.maxSharedUsers = maxSharedUsers;
this.logoUrl = logoUrl;
}
public Subscription toDomain() {
return Subscription.builder()
.id(id)
.name(name)
.description(description)
.category(category)
.price(price)
.maxSharedUsers(maxSharedUsers)
.logoUrl(logoUrl)
.build();
}
}
@@ -0,0 +1,7 @@
package com.unicorn.lifesub.mysub.infra.repository;
import com.unicorn.lifesub.mysub.infra.entity.CategoryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CategoryJpaRepository extends JpaRepository<CategoryEntity, String> {
}
@@ -0,0 +1,9 @@
package com.unicorn.lifesub.mysub.infra.repository;
import com.unicorn.lifesub.mysub.infra.entity.MySubscriptionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface MySubscriptionJpaRepository extends JpaRepository<MySubscriptionEntity, Long> {
List<MySubscriptionEntity> findByUserId(String userId);
}
@@ -0,0 +1,11 @@
package com.unicorn.lifesub.mysub.infra.repository;
import com.unicorn.lifesub.mysub.infra.entity.SubscriptionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// SubscriptionJpaRepository.java에 메서드 추가
public interface SubscriptionJpaRepository extends JpaRepository<SubscriptionEntity, Long> {
List<SubscriptionEntity> findByCategory(String category);
}
@@ -0,0 +1,26 @@
server:
port: ${SERVER_PORT:8082}
spring:
application:
name: mysub-service
datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:mysub}
username: ${POSTGRES_USER:postgres}
password: ${POSTGRES_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:validate}
show-sql: ${JPA_SHOW_SQL:false}
properties:
hibernate:
format_sql: true
allowedorigins: ${ALLOWED_ORIGINS:*}
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /api-docs