diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml index 8268975b..b5a23a02 100644 --- a/.idea/dbnavigator.xml +++ b/.idea/dbnavigator.xml @@ -13,7 +13,7 @@ - + diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/controller/CategorizedAnswerController.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/controller/CategorizedAnswerController.java new file mode 100644 index 00000000..9f13d083 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/controller/CategorizedAnswerController.java @@ -0,0 +1,38 @@ +package com.web.baebaeBE.domain.categorized.answer.controller; + +import com.web.baebaeBE.domain.categorized.answer.controller.api.CategorizedAnswerApi; +import com.web.baebaeBE.domain.categorized.answer.dto.CategorizedAnswerRequest; +import com.web.baebaeBE.domain.categorized.answer.dto.CategorizedAnswerResponse; +import com.web.baebaeBE.domain.categorized.answer.service.CategorizedAnswerService; +import com.web.baebaeBE.global.authorization.annotation.AuthorizationAnswer; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/categorizedAnswer") +public class CategorizedAnswerController implements CategorizedAnswerApi { + + private final CategorizedAnswerService categorizedAnswerService; + + + @GetMapping("{answerId}") + @AuthorizationAnswer + public ResponseEntity> getCategoriesByAnswerId( + @PathVariable Long answerId + ) { + return ResponseEntity.ok(categorizedAnswerService.getCategoriesByAnswerId(answerId)); + } + + @PutMapping("/{answerId}") + @AuthorizationAnswer + public ResponseEntity updateCategoriesByAnswerId(@PathVariable Long answerId, @RequestBody CategorizedAnswerRequest.CategoryList categoryIds) { + categorizedAnswerService.updateCategoriesByAnswerId(answerId, categoryIds); + return ResponseEntity.noContent().build(); + } + + +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/controller/api/CategorizedAnswerApi.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/controller/api/CategorizedAnswerApi.java new file mode 100644 index 00000000..628d12ab --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/controller/api/CategorizedAnswerApi.java @@ -0,0 +1,56 @@ +package com.web.baebaeBE.domain.categorized.answer.controller.api; + +import com.web.baebaeBE.domain.categorized.answer.dto.CategorizedAnswerRequest; +import com.web.baebaeBE.domain.categorized.answer.dto.CategorizedAnswerResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +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 io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +@Tag(name = "CategorizedAnswer", description = "카테고리 내부의 피드에 관련된 API") +public interface CategorizedAnswerApi { + + @Operation(summary = "피드가 속한 카테고리 조회", + description = "Answer ID를 받아 해당 Answer에 연결된 모든 카테고리를 조회합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "조회 성공") + }) + @GetMapping("{answerId}") + ResponseEntity> getCategoriesByAnswerId( + @Parameter(description = "Answer의 ID", required = true) @PathVariable Long answerId + ); + + + @Operation(summary = "피드가 속한 카테고리 수정", + description = "Answer ID와 Category ID 리스트를 받아 피드가 속할 카테고리 정보를 수정합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @Parameter( + in = ParameterIn.HEADER, + name = "Authorization", required = true, + schema = @Schema(type = "string"), + description = "Bearer [Access 토큰]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "수정 성공") + }) + @PutMapping("/{answerId}") + public ResponseEntity updateCategoriesByAnswerId(@PathVariable Long answerId, @RequestBody CategorizedAnswerRequest.CategoryList categoryIds) ; +} \ No newline at end of file diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/dto/CategorizedAnswerRequest.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/dto/CategorizedAnswerRequest.java new file mode 100644 index 00000000..7edbaf63 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/dto/CategorizedAnswerRequest.java @@ -0,0 +1,20 @@ +package com.web.baebaeBE.domain.categorized.answer.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +public class CategorizedAnswerRequest { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class CategoryList{ + private List categoryIds; + + } +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/dto/CategorizedAnswerResponse.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/dto/CategorizedAnswerResponse.java new file mode 100644 index 00000000..2a0fbc99 --- /dev/null +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/dto/CategorizedAnswerResponse.java @@ -0,0 +1,31 @@ +package com.web.baebaeBE.domain.categorized.answer.dto; + +import com.web.baebaeBE.domain.category.dto.CategoryResponse; +import com.web.baebaeBE.domain.category.entity.Category; +import lombok.*; + +import java.util.List; +import java.util.stream.Collectors; + +public class CategorizedAnswerResponse { + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CategoryInformationResponse { + private Long categoryId; + private String categoryName; + private String categoryImage; + + public static CategorizedAnswerResponse.CategoryInformationResponse of(Category category) { + return CategorizedAnswerResponse.CategoryInformationResponse.builder() + .categoryId(category.getId()) + .categoryName(category.getCategoryName()) + .categoryImage(category.getCategoryImage()) + .build(); + } + } + +} diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/entity/CategorizedAnswer.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/entity/CategorizedAnswer.java index 33a365e8..43735700 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/entity/CategorizedAnswer.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/entity/CategorizedAnswer.java @@ -4,6 +4,8 @@ import com.web.baebaeBE.domain.category.entity.Category; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalDateTime; @@ -18,14 +20,17 @@ public class CategorizedAnswer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "categorized_answer_id") private Long id; @ManyToOne @JoinColumn(name = "category_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Category category; @ManyToOne @JoinColumn(name = "answer_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Answer answer; } \ No newline at end of file diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/repository/CategorizedAnswerRepository.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/repository/CategorizedAnswerRepository.java index 9937f5ee..22617964 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/repository/CategorizedAnswerRepository.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/repository/CategorizedAnswerRepository.java @@ -7,8 +7,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface CategorizedAnswerRepository extends JpaRepository { Page findByAnswer_Member_IdAndCategory_Id(Long memberId, Long categoryId, Pageable pageable); Page findByAnswer_Member_Id(Long memberId, Pageable pageable); + List findAllByAnswerId(Long answerId); } diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/service/CategorizedAnswerService.java b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/service/CategorizedAnswerService.java index 83287077..75ab5171 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/service/CategorizedAnswerService.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/domain/categorized/answer/service/CategorizedAnswerService.java @@ -1,9 +1,18 @@ package com.web.baebaeBE.domain.categorized.answer.service; import com.web.baebaeBE.domain.answer.dto.AnswerDetailResponse; +import com.web.baebaeBE.domain.answer.entity.Answer; +import com.web.baebaeBE.domain.answer.exception.AnswerError; import com.web.baebaeBE.domain.answer.repository.AnswerMapper; +import com.web.baebaeBE.domain.answer.repository.AnswerRepository; +import com.web.baebaeBE.domain.categorized.answer.dto.CategorizedAnswerRequest; +import com.web.baebaeBE.domain.categorized.answer.dto.CategorizedAnswerResponse; import com.web.baebaeBE.domain.categorized.answer.entity.CategorizedAnswer; import com.web.baebaeBE.domain.categorized.answer.repository.CategorizedAnswerRepository; +import com.web.baebaeBE.domain.category.entity.Category; +import com.web.baebaeBE.domain.category.exception.CategoryException; +import com.web.baebaeBE.domain.category.repository.CategoryRepository; +import com.web.baebaeBE.global.error.exception.BusinessException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -11,6 +20,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + @Service @Slf4j @RequiredArgsConstructor @@ -18,6 +32,25 @@ public class CategorizedAnswerService { private final CategorizedAnswerRepository categorizedAnswerRepository; private final AnswerMapper answerMapper; + private final CategoryRepository categoryRepository; + private final AnswerRepository answerRepository; + + public List getCategoriesByAnswerId(Long answerId) { + List categorizedAnswers = categorizedAnswerRepository.findAllByAnswerId(answerId); + + for(CategorizedAnswer categorizedAnswer : categorizedAnswers) { + log.info("categorizedAnswer : {}", categorizedAnswer.getCategory().getId()); + } + List categoryInformationResponses = new ArrayList<>(); + + for(CategorizedAnswer categorizedAnswer : categorizedAnswers) { + categoryInformationResponses.add + (CategorizedAnswerResponse.CategoryInformationResponse.of(categorizedAnswer.getCategory())); + } + + return categoryInformationResponses; + } + public Page getAnswersByMemberAndCategory(Long memberId, Long categoryId, Pageable pageable) { Page categorizedAnswers; @@ -27,6 +60,49 @@ public Page getAnswersByMemberAndCategory(Long memberId, L categorizedAnswers = categorizedAnswerRepository.findByAnswer_Member_IdAndCategory_Id(memberId, categoryId, pageable); return categorizedAnswers.map(categorizedAnswer -> answerMapper.toDomain(categorizedAnswer.getAnswer())); + } + + + @Transactional + public void updateCategoriesByAnswerId(Long answerId, CategorizedAnswerRequest.CategoryList categoryList) { + List existingCategorizedAnswers = categorizedAnswerRepository.findAllByAnswerId(answerId); + List existingCategoryIds = existingCategorizedAnswers.stream() + .map(categorizedAnswer -> categorizedAnswer.getCategory().getId()) + .collect(Collectors.toList()); + + List newCategoryIds = categoryList.getCategoryIds(); + + // 추가해야 할 카테고리 ID 계산 + List toAddCategoryIds = newCategoryIds.stream() + .filter(categoryId -> !existingCategoryIds.contains(categoryId)) + .collect(Collectors.toList()); + + // 삭제해야 할 카테고리 ID 계산 + List toRemoveCategoryIds = existingCategoryIds.stream() + .filter(categoryId -> !newCategoryIds.contains(categoryId)) + .collect(Collectors.toList()); + + Answer answer = answerRepository.findByAnswerId(answerId) + .orElseThrow(() -> new BusinessException(AnswerError.NO_EXIST_ANSWER)); + + // 새로운 카테고리 ID에 대해 반복하여 CategorizedAnswer를 생성하고 저장 + for (Long categoryId : toAddCategoryIds) { + Category category = categoryRepository.findById(categoryId) + .orElseThrow(() -> new BusinessException(CategoryException.CATEGORY_NOT_FOUND)); + CategorizedAnswer newCategorizedAnswer = CategorizedAnswer.builder() + .category(category) + .answer(answer) + .build(); + categorizedAnswerRepository.save(newCategorizedAnswer); + } + // 삭제해야 할 카테고리 ID에 대해 반복하여 해당 CategorizedAnswer를 삭제 + for (Long categoryId : toRemoveCategoryIds) { + CategorizedAnswer categorizedAnswerToRemove = existingCategorizedAnswers.stream() + .filter(categorizedAnswer -> categorizedAnswer.getCategory().getId().equals(categoryId)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("No CategorizedAnswer found for the given category id")); + categorizedAnswerRepository.delete(categorizedAnswerToRemove); + } } } \ No newline at end of file diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/global/config/SecurityConfig.java b/baebae-BE/src/main/java/com/web/baebaeBE/global/config/SecurityConfig.java index ddc44bd5..ba198413 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/global/config/SecurityConfig.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/global/config/SecurityConfig.java @@ -5,6 +5,9 @@ import com.web.baebaeBE.domain.login.service.OAuth2UserCustomService; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; @@ -22,8 +25,10 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import javax.annotation.PostConstruct; import java.util.Arrays; import java.util.Collections; +import java.util.List; import static com.web.baebaeBE.global.security.SecurityConstants.NO_AUTH_LIST; import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console; @@ -34,8 +39,9 @@ @EnableMethodSecurity public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; - private final OAuth2UserCustomService oAuth2UserCustomService; + @Value("${allowed.origins}") + private String[] allowedOrigins; // Spring Security 제외 목록 (인증,인가 검사 제외) @@ -84,7 +90,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173", "https://api.flipit.co.kr", "https://www.flipit.co.kr", "https://flipit.co.kr")); // 허용할 오리진 설정 + configuration.setAllowedOrigins(List.of(allowedOrigins)); // 허용할 오리진 설정 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH")); // 허용할 HTTP 메소드 설정 configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 HTTP 헤더 설정 configuration.setAllowCredentials(true); // 쿠키를 포함한 요청 허용 설정 diff --git a/baebae-BE/src/main/java/com/web/baebaeBE/global/config/WebConfig.java b/baebae-BE/src/main/java/com/web/baebaeBE/global/config/WebConfig.java index 54737547..f4a29b42 100644 --- a/baebae-BE/src/main/java/com/web/baebaeBE/global/config/WebConfig.java +++ b/baebae-BE/src/main/java/com/web/baebaeBE/global/config/WebConfig.java @@ -1,15 +1,19 @@ package com.web.baebaeBE.global.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { + @Value("${allowed.origins}") + private String[] allowedOrigins; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:5173", "https://api.flipit.co.kr", "https://www.flipit.co.kr", "https://flipit.co.kr") + .allowedOrigins(allowedOrigins) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") // 모든 헤더 허용 .allowCredentials(true)