diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2a631ef7..771f75ad 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -42,21 +42,7 @@ jobs: ./gradlew clean build shell: bash - # 5. 빌드 성공 시 코멘트 - - name: Comment on PR if Build Succeed - if: success() - uses: actions/github-script@v7 - with: - script: | - const { owner, repo, number } = context.issue; - await github.rest.issues.createComment({ - owner, - repo, - issue_number: number, - body: `✅ 빌드 테스트가 성공적으로 완료되었습니다!` - }); - - # 6. 빌드 실패 시 코멘트 + # 5. 빌드 실패 시 코멘트 - name: Comment on PR if Build Fail if: failure() uses: actions/github-script@v7 diff --git a/build.gradle b/build.gradle index 738d237d..979ec797 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,10 @@ plugins { id 'idea' } +ext { + springCloudVersion = "2023.0.4" +} + group = 'com.woozuda' version = '0.0.1-SNAPSHOT' @@ -35,6 +39,7 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-webflux' // Apache HttpClient 의존성 추가 implementation 'org.apache.httpcomponents:httpclient:4.5.13' // Jackson 라이브러리 (JSON 파싱을 위한) @@ -86,12 +91,24 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' //prometheus(metrics) runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + //OpenFeign: 외부 API 사용 + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.4' + + // html 파싱을 위한 jsoup + implementation 'org.jsoup:jsoup:1.16.1' } tasks.named('test') { useJUnitPlatform() } +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion" + } +} + //QueryDSL 설정부 시작 /* diff --git a/src/main/java/com/woozuda/backend/BackendApplication.java b/src/main/java/com/woozuda/backend/BackendApplication.java index 9a1d7771..82a07b83 100644 --- a/src/main/java/com/woozuda/backend/BackendApplication.java +++ b/src/main/java/com/woozuda/backend/BackendApplication.java @@ -6,7 +6,9 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @EnableJpaAuditing @SpringBootApplication public class BackendApplication { diff --git a/src/main/java/com/woozuda/backend/account/controller/AdminController.java b/src/main/java/com/woozuda/backend/account/controller/AdminController.java index dd9ced10..94d78b5b 100644 --- a/src/main/java/com/woozuda/backend/account/controller/AdminController.java +++ b/src/main/java/com/woozuda/backend/account/controller/AdminController.java @@ -1,6 +1,7 @@ package com.woozuda.backend.account.controller; +import com.woozuda.backend.aop.LogExecutionTime; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -12,6 +13,7 @@ public String adminP(){ return "admin controller"; } + @LogExecutionTime @GetMapping("/account/sample/alluser") public String allP(){ return "all user can access this page!"; @@ -19,5 +21,4 @@ public String allP(){ @GetMapping("/account/sample/user") public String userP(){ return "user can access this page! ";} - } diff --git a/src/main/java/com/woozuda/backend/account/controller/JoinController.java b/src/main/java/com/woozuda/backend/account/controller/JoinController.java index 900d9c14..84042f5b 100644 --- a/src/main/java/com/woozuda/backend/account/controller/JoinController.java +++ b/src/main/java/com/woozuda/backend/account/controller/JoinController.java @@ -2,8 +2,8 @@ import com.woozuda.backend.account.dto.JoinDTO; import com.woozuda.backend.account.service.JoinService; -import com.woozuda.backend.exception.InvalidEmailException; -import com.woozuda.backend.exception.UsernameAlreadyExistsException; +import com.woozuda.backend.exception.account.InvalidEmailException; +import com.woozuda.backend.exception.account.UsernameAlreadyExistsException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -21,15 +21,7 @@ public JoinController(JoinService joinService){ @PostMapping("/join") public ResponseEntity joinProcess(@RequestBody JoinDTO joinDTO){ - - try { - joinService.joinProcess(joinDTO); - }catch(InvalidEmailException e){ - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build(); - }catch(UsernameAlreadyExistsException e) { - return ResponseEntity.status(HttpStatus.CONFLICT).build(); - } - + joinService.joinProcess(joinDTO); return ResponseEntity.status(HttpStatus.OK).build(); } } diff --git a/src/main/java/com/woozuda/backend/account/controller/LogoutController.java b/src/main/java/com/woozuda/backend/account/controller/LogoutController.java new file mode 100644 index 00000000..025dfb17 --- /dev/null +++ b/src/main/java/com/woozuda/backend/account/controller/LogoutController.java @@ -0,0 +1,30 @@ +package com.woozuda.backend.account.controller; + + +import com.woozuda.backend.account.service.LogoutService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +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.RestController; + +@RequestMapping("/api/logout") +@RequiredArgsConstructor +@RestController +public class LogoutController { + + private final LogoutService logoutService; + + @GetMapping("") + public ResponseEntity logout(){ + + ResponseCookie expiredCookie = logoutService.logout(); + + return ResponseEntity.status(HttpStatus.OK) + .header(HttpHeaders.SET_COOKIE, expiredCookie.toString()) + .build(); + } +} diff --git a/src/main/java/com/woozuda/backend/account/dto/JoinDTO.java b/src/main/java/com/woozuda/backend/account/dto/JoinDTO.java index f72b9a84..37cb5aa1 100644 --- a/src/main/java/com/woozuda/backend/account/dto/JoinDTO.java +++ b/src/main/java/com/woozuda/backend/account/dto/JoinDTO.java @@ -1,13 +1,20 @@ package com.woozuda.backend.account.dto; +import com.woozuda.backend.account.entity.AiType; import com.woozuda.backend.account.entity.UserEntity; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @Getter @Setter +@AllArgsConstructor public class JoinDTO { String username; String password; + + public static JoinDTO transDTO(UserEntity userEntity){ + return new JoinDTO(userEntity.getUsername(), userEntity.getPassword()); + } } diff --git a/src/main/java/com/woozuda/backend/account/entity/UserEntity.java b/src/main/java/com/woozuda/backend/account/entity/UserEntity.java index 9f7f1c59..6f064abe 100644 --- a/src/main/java/com/woozuda/backend/account/entity/UserEntity.java +++ b/src/main/java/com/woozuda/backend/account/entity/UserEntity.java @@ -47,6 +47,6 @@ public class UserEntity extends BaseTimeEntity { private String provider; public static UserEntity transEntity(JoinDTO joinDTO){ - return new UserEntity(null, joinDTO.getUsername(), joinDTO.getPassword(), "ROLE_ADMIN", AiType.PICTURE_NOVEL, true, joinDTO.getUsername(), "woozuda"); + return new UserEntity(null, joinDTO.getUsername(), joinDTO.getPassword(), "ROLE_USER", AiType.PICTURE_NOVEL, true, joinDTO.getUsername(), "woozuda"); } } diff --git a/src/main/java/com/woozuda/backend/account/service/CustomOAuth2UserService.java b/src/main/java/com/woozuda/backend/account/service/CustomOAuth2UserService.java index 33e7e66a..6b5f1e91 100644 --- a/src/main/java/com/woozuda/backend/account/service/CustomOAuth2UserService.java +++ b/src/main/java/com/woozuda/backend/account/service/CustomOAuth2UserService.java @@ -36,7 +36,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic if (existData == null) { - UserEntity userEntity = new UserEntity(null, username, null, "ROLE_ADMIN", AiType.PICTURE_NOVEL, true, oAuth2Response.getEmail(), oAuth2Response.getProvider()); + UserEntity userEntity = new UserEntity(null, username, null, "ROLE_USER", AiType.PICTURE_NOVEL, true, oAuth2Response.getEmail(), oAuth2Response.getProvider()); userRepository.save(userEntity); shortLinkUtil.saveShortLink(userEntity); diff --git a/src/main/java/com/woozuda/backend/account/service/CustomUserDetailsService.java b/src/main/java/com/woozuda/backend/account/service/CustomUserDetailsService.java index 38b69a1c..9f078d7a 100644 --- a/src/main/java/com/woozuda/backend/account/service/CustomUserDetailsService.java +++ b/src/main/java/com/woozuda/backend/account/service/CustomUserDetailsService.java @@ -24,6 +24,7 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx log.info("로그인 디버깅 로그 - "); log.info(username); UserEntity userData = userRepository.findByUsername(username); + if(userData != null){ return new CustomUser(userData); } diff --git a/src/main/java/com/woozuda/backend/account/service/JoinService.java b/src/main/java/com/woozuda/backend/account/service/JoinService.java index 5af93d1a..948846b5 100644 --- a/src/main/java/com/woozuda/backend/account/service/JoinService.java +++ b/src/main/java/com/woozuda/backend/account/service/JoinService.java @@ -3,11 +3,10 @@ import com.woozuda.backend.account.dto.JoinDTO; import com.woozuda.backend.account.entity.UserEntity; import com.woozuda.backend.account.repository.UserRepository; -import com.woozuda.backend.exception.InvalidEmailException; -import com.woozuda.backend.exception.UsernameAlreadyExistsException; +import com.woozuda.backend.exception.account.InvalidEmailException; +import com.woozuda.backend.exception.account.UsernameAlreadyExistsException; import com.woozuda.backend.shortlink.util.ShortLinkUtil; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +27,7 @@ public class JoinService { private final ShortLinkUtil shortLinkUtil; @Transactional - public void joinProcess(JoinDTO joinDTO){ + public JoinDTO joinProcess(JoinDTO joinDTO){ if(!isValidEmail(joinDTO.getUsername())){ throw new InvalidEmailException("잘못된 이메일 형식을 입력 했습니다"); @@ -46,15 +45,15 @@ public void joinProcess(JoinDTO joinDTO){ UserEntity data = UserEntity.transEntity(joinDTO); //레포지터리에 entity를 저장합니다 - userRepository.save(data); + UserEntity newUser = userRepository.save(data); // 유저에 대한 숏링크 제작 shortLinkUtil.saveShortLink(data); - + return JoinDTO.transDTO(newUser); } - public static boolean isValidEmail(String username){ + public boolean isValidEmail(String username){ // 이메일 주소 형식이 아닌 경우 false String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"; diff --git a/src/main/java/com/woozuda/backend/account/service/LogoutService.java b/src/main/java/com/woozuda/backend/account/service/LogoutService.java new file mode 100644 index 00000000..91ae7b56 --- /dev/null +++ b/src/main/java/com/woozuda/backend/account/service/LogoutService.java @@ -0,0 +1,28 @@ +package com.woozuda.backend.account.service; + + +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Service; + +@Service +public class LogoutService { + + public ResponseCookie logout(){ + + ResponseCookie expiredCookie = createCookie("Authorization", ""); + return expiredCookie; + } + + private ResponseCookie createCookie(String key, String value) { + + ResponseCookie responseCookie = ResponseCookie.from(key, value) + .httpOnly(false) + .secure(true) + .path("/") + .maxAge(0) + .sameSite("None") + .build(); + return responseCookie; + } +} diff --git a/src/main/java/com/woozuda/backend/ai/config/ChatDALL_EService.java b/src/main/java/com/woozuda/backend/ai/config/ChatDALL_EService.java deleted file mode 100644 index 790dd161..00000000 --- a/src/main/java/com/woozuda/backend/ai/config/ChatDALL_EService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.woozuda.backend.ai.config; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ChatDALL_EService { - - private final RestTemplate restTemplate; - @Value("${openai.api.key}") - private String apiKey; - - public String generateImageUsingDallE(String prompt) { - String apiUrl = "https://api.openai.com/v1/images/generations"; - - // 요청 데이터 구성 - Map requestBody = Map.of( - "prompt", prompt, // 생성할 이미지에 대한 설명 - "n", 1, // 생성할 이미지 개수 - "size", "512x512" // 이미지 크기 - ); - - // HttpHeader에 추가 - org.springframework.http.HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + apiKey); - headers.setContentType(MediaType.APPLICATION_JSON); - - // HttpEntity로 요청과 헤더를 묶어서 전송 - HttpEntity> entity = new HttpEntity<>(requestBody, headers); - - try { - // HTTP 요청 보내기 - ResponseEntity response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, String.class); - - // API 응답이 성공적일 경우 반환된 데이터 리턴 - if (response.getStatusCode() == HttpStatus.OK) { - return response.getBody(); - } else { - throw new RuntimeException("AI 호출 실패: " + response.getStatusCode()); - } - } catch (Exception e) { - throw new RuntimeException("AI 호출 중 오류 발생: " + e.getMessage()); - } - } -} diff --git a/src/main/java/com/woozuda/backend/ai/config/ChatGPTConfig.java b/src/main/java/com/woozuda/backend/ai/config/ChatGPTConfig.java index 08d1e3ed..8dd95c6a 100644 --- a/src/main/java/com/woozuda/backend/ai/config/ChatGPTConfig.java +++ b/src/main/java/com/woozuda/backend/ai/config/ChatGPTConfig.java @@ -6,8 +6,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; -import org.springframework.web.client.RestTemplate; import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.WebClient; /** * ChatGPT API 통합을 위한 설정 클래스입니다. @@ -20,33 +20,17 @@ public class ChatGPTConfig { @Value("${openai.api.key}") private String apiKey; - /** - * RestTemplate 빈 설정. - * ChatGPT API와 같은 외부 서비스에 HTTP 요청을 보낼 때 사용됩니다. - * - * @return RestTemplate 인스턴스 - */ - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); - } - - /** - * ChatGPT API 요청에 사용되는 HTTP 헤더를 설정 - * - * @return HttpHeaders 인스턴스 - */ - @Bean - public HttpHeaders httpHeaders() { - HttpHeaders headers = new HttpHeaders(); + @Value("${openai.api.url}") + private String apiUrl; - // API 키를 x-api-key 헤더에 추가 - headers.set("Authorization", "Bearer " + apiKey); - - // 요청 본문을 JSON 형식으로 설정 - headers.setContentType(MediaType.APPLICATION_JSON); - - return headers; + @Bean + public WebClient webClient() { + return WebClient.builder() + .baseUrl(apiUrl) // API base URL 설정 + .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) // Authorization 헤더 설정 + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // Content-Type 설정 + .build(); } + } \ No newline at end of file diff --git a/src/main/java/com/woozuda/backend/ai/config/ChatGptService.java b/src/main/java/com/woozuda/backend/ai/config/ChatGptService.java index 52e53b86..7a36b891 100644 --- a/src/main/java/com/woozuda/backend/ai/config/ChatGptService.java +++ b/src/main/java/com/woozuda/backend/ai/config/ChatGptService.java @@ -3,9 +3,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; import java.util.Map; @@ -16,10 +16,11 @@ public class ChatGptService { /** * chat GPT 연동하는 서비스 로직 입니다. */ - private final RestTemplate restTemplate; - @Value("${openai.api.key}") - private String apiKey; + private final WebClient webClient; + + @Value("${openai.api.url}") + private String apiUrl; private static final int MAX_CONTEXT_TOKENS = 4096; // max 토큰 값 설정 @@ -34,8 +35,6 @@ public String analyzeDiaryUsingGPT(String systemMessage, String userMessage) { // maxTokens가 0보다 작지 않도록 확인 maxTokens = Math.max(maxTokens, 50); // 최소 50 토큰은 남겨두기 - String apiUrl = "https://api.openai.com/v1/chat/completions"; // 실제 ChatGPT API URL - // 요청 데이터 구성 Map requestBody = Map.of( "model", "gpt-3.5-turbo", // GPT 3.5 모델 사용 @@ -46,34 +45,22 @@ public String analyzeDiaryUsingGPT(String systemMessage, String userMessage) { "max_tokens", maxTokens // 응답에 사용할 최대 토큰 수 설정 ); - // HttpHeader에 추가 - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + apiKey); - //headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); - headers.setContentType(MediaType.APPLICATION_JSON); - - // HttpEntity로 요청과 헤더를 묶어서 전송 - HttpEntity> entity = new HttpEntity<>(requestBody, headers); - - try { - // HTTP 요청 보내기 - ResponseEntity response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, String.class); - - // API 응답이 성공적일 경우 반환된 데이터 리턴 - if (response.getStatusCode() == HttpStatus.OK) { - return response.getBody(); - } else { - throw new RuntimeException("AI 호출 실패: " + response.getStatusCode()); - } - } catch (Exception e) { - throw new RuntimeException("AI 호출 중 오류 발생: " + e.getMessage()); - - } + // WebClient로 요청 보내기 + Mono responseMono = webClient.post() + .uri(apiUrl) // 실제 엔드포인트 경로로 수정 + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String.class); + + // 동기적으로 응답을 기다리기 위해 block() 사용 + return responseMono.block(); } + private int estimateTokenCount(String text) { // 간단한 토큰 계산: 한글 기준으로 1.5~2배로 계산 return text.split("\\s+").length * 2; // 한 단어에 대해 2토큰으로 계산 } + } diff --git a/src/main/java/com/woozuda/backend/ai/config/NaverCLOVAConfig.java b/src/main/java/com/woozuda/backend/ai/config/NaverCLOVAConfig.java new file mode 100644 index 00000000..d4fd62e9 --- /dev/null +++ b/src/main/java/com/woozuda/backend/ai/config/NaverCLOVAConfig.java @@ -0,0 +1,58 @@ +package com.woozuda.backend.ai.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@Configuration +@Slf4j +public class NaverCLOVAConfig { + + @Value("${clova.api.key}") + private String apiKey; + + + @Value("${clova.api.url}") + private String apiUrl; + + + @Value("${clova.api.rid}") + private String apiRid; + + @Bean(name = "clovaWebClient") + public WebClient webClient() { + return WebClient.builder().build(); + } + + + + public String analyzeDiaryUsingCLOVA(String systemMessage,String userMessage){ + Map requestBody = Map.of( + "messages", new Object[]{ + Map.of("role", "system", "content", systemMessage), + Map.of("role", "user", "content", userMessage) + } + + ); + + Mono responseMono = webClient().post() + .uri(apiUrl) + .header("Authorization", "Bearer " + apiKey) + .header("X-NCP-CLOVASTUDIO-REQUEST-ID", apiRid) + .header("Content-Type", "application/json") + .header("Accept", "text/event-stream") + .bodyValue(requestBody) + .retrieve() + .bodyToMono(String.class); + + // 동기적으로 응답을 기다리기 위해 block() 사용 + return responseMono.block(); + } + + +} diff --git a/src/main/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryImpl.java b/src/main/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryImpl.java index d66a5327..51d24b5d 100644 --- a/src/main/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryImpl.java +++ b/src/main/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryImpl.java @@ -25,8 +25,8 @@ public Optional findByAiCreation(LocalDate start_date, LocalDate end AiCreation result = query .selectFrom(aiCreation) .join(aiCreation.user, userEntity) - .where(aiCreation.start_date.eq(start_date) // 필드 이름을 실제 엔티티에 맞게 수정 - .and(aiCreation.end_date.eq(end_date)) + .where(aiCreation.start_date.goe(start_date) // 필드 이름을 실제 엔티티에 맞게 수정 + .and(aiCreation.end_date.loe(end_date)) .and(userEntity.username.eq(username))) .fetchOne(); diff --git a/src/main/java/com/woozuda/backend/ai_creation/service/CreationPoetryAnalysisService.java b/src/main/java/com/woozuda/backend/ai_creation/service/CreationPoetryAnalysisService.java index 1cb69202..e9fa22b2 100644 --- a/src/main/java/com/woozuda/backend/ai_creation/service/CreationPoetryAnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_creation/service/CreationPoetryAnalysisService.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.woozuda.backend.ai.config.ChatDALL_EService; import com.woozuda.backend.ai.config.ChatGptService; import com.woozuda.backend.ai_creation.dto.AiCreationDTO; import com.woozuda.backend.forai.dto.NonRetroNoteEntryResponseDto; @@ -12,6 +11,8 @@ import java.time.DayOfWeek; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -21,11 +22,9 @@ @RequiredArgsConstructor public class CreationPoetryAnalysisService { private final ChatGptService chatGptService; - private final ChatDALL_EService chatDALL_EService; private final ObjectMapper objectMapper; private final AiCreationService aiCreationService; - public void analyze(List diaryList , String username) { if (diaryList == null || diaryList.isEmpty()) { throw new IllegalArgumentException("분석할 일기 데이터가 없습니다."); @@ -50,20 +49,19 @@ public void analyze(List diaryList , String userna 당신은 분석 도우미입니다. 사용자의 일기를 분석하고 다음과 같은 정보를 제공하세요: 1. 일기를 읽고 창작으로 시를 간단하게 써주세요. 2. **중요** 분석이 불가능한 경우 비슷한 데이터라도 출력해주세요. 절대 Null 반환 금지 - 3. 위의 내용을 포함하여 각 항목처럼 반환해주세요. 예: - image_url : 이미지를 url 을 반환해주세요. - text : 줄바꿈(/n) 없이 창작한 시를 반환해주세요. + 3. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 4. text 는 첫 줄만 띄어쓰기를 꼭 하고, 줄바꿈 없이 230자 이하로 창착한 시를 출력해주세요. + 5. 위의 내용을 포함하여 각 항목처럼 반환해주세요. 예: + start_date : 2024-12-01 + end_date : 2024-12-31 + text : 비 내리는 창밖, 커피 한 잔에 관해 비 소리는 마음 정리 음악 시간 멈춤 """; - log.info("사용자 메시지 내용 Diary: {}", userMessage.toString()); - + log.info("창작 메세지 Creation: {}", userMessage.toString()); // ChatGPT API 호출 String response = chatGptService.analyzeDiaryUsingGPT(systemMessage, userMessage.toString()); - //String img = chatDALL_EService.generateImageUsingDallE(response); - - // 로그: AI가 응답한 내용 출력 log.info("AI 응답 내용: {}", response); - //log.info("달이 응답 내용: {}", img); + // GPT 응답을 AiDiaryDTO로 매핑 AiCreationDTO aiCreationDTO = mapResponseToAiDiaryDTO(response , username); @@ -73,6 +71,8 @@ public void analyze(List diaryList , String userna } + + private AiCreationDTO mapResponseToAiDiaryDTO(String response ,String username) { try { JsonNode root = objectMapper.readTree(response); @@ -82,22 +82,26 @@ private AiCreationDTO mapResponseToAiDiaryDTO(String response ,String username) JsonNode contentNode = messageNode.path("content"); String content = contentNode.asText(); - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); // 항목 추출 String creationType = "WRITING"; - String image_url = extractValue(content, "image_url"); - String text = extractValue(content, "text"); + String img = extractValue(content, "text"); + String text = img; + + // 이제 text에는 줄바꿈 없이 공백으로만 구분된 텍스트가 저장됩니다. + System.out.println(text); String visibility = "PRIVATE"; return new AiCreationDTO( - startDate, - endDate, + start_date, + end_date, creationType, - image_url, text, + img, visibility, username ); @@ -106,7 +110,18 @@ private AiCreationDTO mapResponseToAiDiaryDTO(String response ,String username) throw new RuntimeException("응답 매핑 중 오류 발생: " + e.getMessage()); } } - + private LocalDate convertStringToDate(String date) { + if (date != null) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: '{}'. 기본값을 사용합니다.", date); + return LocalDate.now(); // 기본값 설정 + } + } private String extractValue(String content, String key) { if (content == null || content.isEmpty()) { log.warn("내용이 비어 있음: {}", key); @@ -126,4 +141,6 @@ private String extractValue(String content, String key) { log.warn("값 추출 실패: {}", key); return "분석불가"; // 기본값 설정 } + + } diff --git a/src/main/java/com/woozuda/backend/ai_creation/service/CreationWritingAnalysisService.java b/src/main/java/com/woozuda/backend/ai_creation/service/CreationWritingAnalysisService.java index 5b28c3ac..3c3785b0 100644 --- a/src/main/java/com/woozuda/backend/ai_creation/service/CreationWritingAnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_creation/service/CreationWritingAnalysisService.java @@ -11,6 +11,8 @@ import java.time.DayOfWeek; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -22,6 +24,7 @@ public class CreationWritingAnalysisService { private final ChatGptService chatGptService; private final ObjectMapper objectMapper; private final AiCreationService aiCreationService; + public void analyze(List diaryList , String username) { if (diaryList == null || diaryList.isEmpty()) { throw new IllegalArgumentException("분석할 일기 데이터가 없습니다."); @@ -46,9 +49,12 @@ public void analyze(List diaryList , String userna 당신은 분석 도우미입니다. 사용자의 일기를 분석하고 다음과 같은 정보를 제공하세요: 1. 일기를 읽고 창작으로 시를 간단하게 써주세요. 2. **중요** 분석이 불가능한 경우 비슷한 데이터라도 출력해주세요. 절대 Null 반환 금지 - 3. 위의 내용을 포함하여 각 항목처럼 반환해주세요. 예: - image_url : 이미지를 url 을 반환해주세요. - text : 줄바꿈(/n) 없이 창작한 시를 반환해주세요. + 3. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 4. text 는 줄바꿈 없이 230 이하로 창착한 글을 출력해주세요. + 5. 위의 내용을 포함하여 각 항목처럼 반환해주세요. 예: + start_date :2024-12-01 + end_date :2024-12-31 + text : 오늘의 내 기분은 그렇게 좋지 않았다. """; log.info("사용자 메시지 내용 Diary: {}", userMessage.toString()); @@ -75,21 +81,22 @@ private AiCreationDTO mapResponseToAiDiaryDTO(String response ,String username) JsonNode contentNode = messageNode.path("content"); String content = contentNode.asText(); - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 - + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); // 항목 추출 String creationType = "WRITING"; - String image_url = extractValue(content, "image_url"); - String text = extractValue(content, "text"); + String img = extractValue(content, "text"); + String text = img; + String visibility = "PRIVATE"; return new AiCreationDTO( - startDate, - endDate, + start_date, + end_date, creationType, - image_url, text, + img, visibility, username ); @@ -98,7 +105,18 @@ private AiCreationDTO mapResponseToAiDiaryDTO(String response ,String username) throw new RuntimeException("응답 매핑 중 오류 발생: " + e.getMessage()); } } - + private LocalDate convertStringToDate(String date) { + if (date != null) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: '{}'. 기본값을 사용합니다.", date); + return LocalDate.now(); // 기본값 설정 + } + } private String extractValue(String content, String key) { if (content == null || content.isEmpty()) { log.warn("내용이 비어 있음: {}", key); diff --git a/src/main/java/com/woozuda/backend/ai_diary/controller/AIDiaryController.java b/src/main/java/com/woozuda/backend/ai_diary/controller/AIDiaryController.java index ae437615..28a02293 100644 --- a/src/main/java/com/woozuda/backend/ai_diary/controller/AIDiaryController.java +++ b/src/main/java/com/woozuda/backend/ai_diary/controller/AIDiaryController.java @@ -4,6 +4,7 @@ import com.woozuda.backend.ai_diary.dto.AiDiaryResponseDTO; import com.woozuda.backend.ai_diary.service.AiDiaryService; import com.woozuda.backend.ai_diary.service.DiaryAnalysisService; +import com.woozuda.backend.ai_diary.service.DiaryAnalysisServiceNAVER; import com.woozuda.backend.forai.dto.NonRetroNoteEntryResponseDto; import com.woozuda.backend.forai.service.CustomeNoteRepoForAiService; import lombok.RequiredArgsConstructor; @@ -24,7 +25,7 @@ public class AIDiaryController { private final DiaryAnalysisService diaryAnalysisService; private final AiDiaryService aiDiaryService; private final CustomeNoteRepoForAiService customeNoteRepoForAiService; - + private final DiaryAnalysisServiceNAVER diaryAnalysisServiceNAVER; @GetMapping("/count") public ResponseEntity getDiaryCount( @RequestParam("start_date") LocalDate start_date, @@ -65,7 +66,7 @@ public ResponseEntity analyzeDiary( } // 일기 분석 실행! diaryAnalysisService.analyzeDiary(diaryList, username); - + //diaryAnalysisServiceNAVER.analyzeDiary(diaryList,username); // 정상적인 경우는 OK 상태와 함께 성공 메시지 또는 데이터를 반환 return ResponseEntity.ok("일기 분석 성공"); } diff --git a/src/main/java/com/woozuda/backend/ai_diary/entity/AiDiary.java b/src/main/java/com/woozuda/backend/ai_diary/entity/AiDiary.java index 6c8f4dc7..295ee1cb 100644 --- a/src/main/java/com/woozuda/backend/ai_diary/entity/AiDiary.java +++ b/src/main/java/com/woozuda/backend/ai_diary/entity/AiDiary.java @@ -12,7 +12,7 @@ @Entity @Table(name = "ai_diary_rep") -@Getter +@Getter @Setter @NoArgsConstructor @AllArgsConstructor public class AiDiary extends BaseTimeEntity{ diff --git a/src/main/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryImpl.java b/src/main/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryImpl.java index 2ed0660b..ed026a43 100644 --- a/src/main/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryImpl.java +++ b/src/main/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryImpl.java @@ -23,8 +23,8 @@ public Optional findByAiDiary(LocalDate start_date, LocalDate end_date, AiDiary result = query .selectFrom(aiDiary) .join(aiDiary.user, userEntity) - .where(aiDiary.start_date.eq(start_date) // 필드 이름을 실제 엔티티에 맞게 수정 - .and(aiDiary.end_date.eq(end_date)) + .where(aiDiary.start_date.goe(start_date) // 필드 이름을 실제 엔티티에 맞게 수정 + .and(aiDiary.end_date.loe(end_date)) .and(userEntity.username.eq(username))) .fetchOne(); diff --git a/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisService.java b/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisService.java index d4cb0fe2..4ea3e841 100644 --- a/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisService.java @@ -9,7 +9,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; @@ -56,30 +55,30 @@ public void analyzeDiary(List diaryList , String u - `positive`와 `denial`의 합은 꼭 100%가 되어야 합니다. 절대 Null 과 0.0을 출력하지 마세요. 5. 개선 사항이나 추천 행동(suggestion)을 반드시 작성하세요. 6. **중요** 분석이 불가능한 경우 비슷한 데이터라도 출력해주세요. 절대 Null 반환 금지 - 7. 위의 내용을 포함하여 각 항목을 반환해주세요. 예: + 7. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 8. 위의 내용을 포함하여 각 항목을 반환해주세요. 예: start_date: 2024-12-01 end_date: 2024-12-31 - place: "장소1, 장소2" - activity: "활동1, 활동2" - emotion: "주요감정1" , "주요감정2" - weather: "비가 올때는 눈물이 난다. 날씨가 맑을때 기분이 좋다." + place: 장소1, 장소2 + activity: 활동1, 활동2 + emotion: 주요감정1" , "주요감정2 + weather: 비가 올때는 눈물이 난다. 날씨가 맑을때 기분이 좋다. weekdayAt: 50.0 weekendAt: 50.0 positive: 80.0 denial: 20.0 - suggestion: "일정 속에서 조금 더 휴식을 취하고, 자신만의 시간을 갖는 것이 중요해 보입니다." + suggestion: 일정 속에서 조금 더 휴식을 취하고, 자신만의 시간을 갖는 것이 중요해 보입니다. """; - log.info("사용자 메시지 내용 Diary: {}", userMessage.toString()); + log.info("사용자 메시지 내용 Diary: {}", userMessage); // ChatGPT API 호출 String response = chatGptService.analyzeDiaryUsingGPT(systemMessage, userMessage.toString()); + // 로그: AI가 응답한 내용 출력 log.info("AI 응답 내용: {}", response); - // GPT 응답을 AiDiaryDTO로 매핑 AiDiaryDTO aiDiaryDTO = mapResponseToAiDiaryDTO(response , username); - // DB에 저장 aiDiaryService.saveAiDiary(aiDiaryDTO); @@ -89,20 +88,16 @@ private AiDiaryDTO mapResponseToAiDiaryDTO(String response ,String username) { try { JsonNode root = objectMapper.readTree(response); - // "choices" 배열 가져오기 JsonNode choicesNode = root.path("choices"); - // 첫 번째 요소 가져오기 JsonNode firstChoiceNode = choicesNode.get(0); - // "message" 필드 가져오기 JsonNode messageNode = firstChoiceNode.path("message"); - // "content" 필드 가져오기 JsonNode contentNode = messageNode.path("content"); - // content 값을 String으로 변환 String content = contentNode.asText(); - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); // 항목 추출 String place = extractValue(content, "place"); @@ -125,8 +120,8 @@ private AiDiaryDTO mapResponseToAiDiaryDTO(String response ,String username) { String suggestion = extractValue(content, "suggestion"); return new AiDiaryDTO( - startDate, - endDate, + start_date, + end_date, place, activity, emotion, @@ -143,6 +138,17 @@ private AiDiaryDTO mapResponseToAiDiaryDTO(String response ,String username) { throw new RuntimeException("응답 매핑 중 오류 발생: " + e.getMessage()); } } + private LocalDate convertStringToDate(String date) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: '{}'. 기본값을 사용합니다.", date); + return LocalDate.now(); // 기본값 설정 + } + } private float convertStringToFloat(String value) { if (value == null || value.isEmpty()) { diff --git a/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisServiceNAVER.java b/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisServiceNAVER.java new file mode 100644 index 00000000..39cab5ae --- /dev/null +++ b/src/main/java/com/woozuda/backend/ai_diary/service/DiaryAnalysisServiceNAVER.java @@ -0,0 +1,150 @@ +package com.woozuda.backend.ai_diary.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woozuda.backend.ai.config.NaverCLOVAConfig; +import com.woozuda.backend.ai_diary.dto.AiDiaryDTO; +import com.woozuda.backend.forai.dto.NonRetroNoteEntryResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DiaryAnalysisServiceNAVER { + private final ObjectMapper objectMapper; + private final AiDiaryService aiDiaryService; + private final NaverCLOVAConfig naverCLOVAConfig; + + public void analyzeDiary(List diaryList , String username) { + if (diaryList == null || diaryList.isEmpty()) { + throw new IllegalArgumentException("분석할 일기 데이터가 없습니다."); + } + + StringBuilder userMessage = new StringBuilder(); + + // 일기 내용 반복문 + for (NonRetroNoteEntryResponseDto diary : diaryList) { + userMessage.append("type: ").append(diary.getType()).append("\n"); + userMessage.append("id: ").append(diary.getId()).append("\n"); + userMessage.append("title: ").append(diary.getTitle()).append("\n"); + userMessage.append("date: ").append(diary.getDate()).append("\n"); + userMessage.append("weather: ").append(diary.getWeather() != null ? diary.getWeather() : "없음").append("\n"); + userMessage.append("season: ").append(diary.getSeason() != null ? diary.getSeason() : "없음").append("\n"); + userMessage.append("feeling: ").append(diary.getFeeling() != null ? diary.getFeeling() : "없음").append("\n"); + userMessage.append("content: ").append(diary.getContent()).append("\n\n"); + } + + // 프롬프트 정의 + String systemMessage = """ + 당신은 분석 도우미입니다. 사용자의 일기를 분석하고 다음과 같은 정보를 제공하세요: + 1. 주요 장소(place), 주요 활동(activity), 주요 감정(emotion)을 반드시 분석해 값을 반환해서 목록 형식으로 제공 + 2. 사용자가 제공한 날씨 데이터를 감정을 기반으로 분석 결과를 작성하세요. + 3. 평일/주말 비율(weekdayAt, weekendAt)을 분석이 가능하면 각각 반드시 백분율로 출력해주세요. + - `weekdayAt`과 `weekendAt`의 합은 꼭 100%가 되어야 합니다. 절대 Null 과 0.0을 출력하지 마세요. + 4. 긍정적 감정(positive)과 부정적 감정(denial)이 분석이 가능하면 긍정적 감정과 부정적 감정의 비율을 각각 백분율로 출력해주세요. + - `positive`와 `denial`의 합은 꼭 100%가 되어야 합니다. 절대 Null 과 0.0을 출력하지 마세요. + 5. 개선 사항이나 추천 행동(suggestion)을 반드시 작성하세요. + 6. **중요** 분석이 불가능한 경우 비슷한 데이터라도 출력해주세요. 절대 Null 반환 금지 + 7. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 8. 위의 내용을 포함하여 각 항목을 반환해주세요. 그리고 응답에서 `id`를 제거해주고 event에는 result` 부분만 포함하여 주세요." 예: + start_date: 2024-12-01 + end_date: 2024-12-31 + place: 장소1, 장소2 + activity: 활동1, 활동2 + emotion: 주요감정1 , 주요감정2 + weather: 비가 올때는 눈물이 난다. 날씨가 맑을때 기분이 좋다. + weekdayAt: 50.0 + weekendAt: 50.0 + positive: 80.0 + denial: 20.0 + suggestion: 일정 속에서 조금 더 휴식을 취하고, 자신만의 시간을 갖는 것이 중요해 보입니다. + """; + log.info("사용자 메시지 내용 Diary: {}", userMessage.toString()); + + // 클로바 호출 + String response = naverCLOVAConfig.analyzeDiaryUsingCLOVA(systemMessage,userMessage.toString()); + + log.info("클로바 호출 성공 "); + + // 로그: AI가 응답한 내용 출력 + log.info("AI 응답 내용: {}", response); + + // AiDiaryDTO aiDiaryDTO = mapResponseToAiDiaryDTO(response , username); + + // DB에 저장 + //aiDiaryService.saveAiDiary(aiDiaryDTO); + + } + public AiDiaryDTO mapResponseToAiDiaryDTO(String response , String username) { + try { + // "data:" 제거 후 JSON 변환 + String jsonResponse = response.replaceFirst("^data:", "").trim(); + JsonNode root = objectMapper.readTree(jsonResponse); + + // "message" -> "content" 추출 + String content = root.path("message").path("content").asText(); + + // 값 추출 + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + String place = extractValue(content, "place"); + String activity = extractValue(content, "activity"); + String emotion = extractValue(content, "emotion"); + String weather = extractValue(content, "weather"); + String suggestion = extractValue(content, "suggestion"); + + // 숫자 변환 + float positive = convertStringToFloat(extractValue(content, "positive")); + float denial = convertStringToFloat(extractValue(content, "denial")); + float weekdayAt = convertStringToFloat(extractValue(content, "weekdayAt")); + float weekendAt = convertStringToFloat(extractValue(content, "weekendAt")); + + // 날짜 변환 + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); + + // DTO 반환 + return new AiDiaryDTO( + start_date, + end_date, + place, + activity, + emotion, + weather, + weekdayAt, + weekendAt, + positive, + denial, + suggestion, + username + ); + } catch (Exception e) { + throw new RuntimeException("JSON 파싱 중 오류 발생", e); + } + } + + // 문자열에서 특정 키의 값을 추출하는 메서드 + private String extractValue(String content, String key) { + String pattern = key + "\\s*:\\s*\"?(.*?)\"?\\n"; + java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern); + java.util.regex.Matcher matcher = regex.matcher(content); + return matcher.find() ? matcher.group(1).trim() : ""; + } + + // 문자열을 LocalDate로 변환 + private LocalDate convertStringToDate(String dateStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return LocalDate.parse(dateStr, formatter); + } + + // 문자열을 float으로 변환 + private float convertStringToFloat(String floatStr) { + return floatStr.isEmpty() ? 0.0f : Float.parseFloat(floatStr); + } +} diff --git a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_4fs_AnalysisService.java b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_4fs_AnalysisService.java index f74a738e..e37383c6 100644 --- a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_4fs_AnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_4fs_AnalysisService.java @@ -47,7 +47,8 @@ public void analyzeAirecall(List recallList , String 4. improvementSuggest : 사용자의 행동이나 패턴을 기반으로 개선할 점을 두 줄 정도로 제시해 주세요. 5. utilizationTips : 사용자가 자신의 행동을 더 효율적으로 개선하거나 활용할 수 있는 방법을 두 줄 정도로 제시해 주세요. 6. **중요**절대 모든 값의 Null 및 0을 반환하지 마세요. 비슷한 분석 결과값을 반환해주세요. - 7. 위의 내용을 포함하여 각 항목을 한번만 반환해주세요. 예: + 7. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 8. 위의 내용을 포함하여 각 항목을 한번만 반환해주세요. 예: start_date: 2024-12-01 end_date: 2024-12-31 type : 4FS @@ -83,10 +84,10 @@ private Airecall_4fs_DTO mapResponseToAirecall(String response , String username /** * 날짜 변경 */ - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 - + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); /** * type 변경 */ @@ -101,8 +102,8 @@ private Airecall_4fs_DTO mapResponseToAirecall(String response , String username return new Airecall_4fs_DTO( airecallType, - startDate, - endDate, + start_date, + end_date, patternAnalysis, positiveBehavior, improvementSuggest, @@ -115,7 +116,18 @@ private Airecall_4fs_DTO mapResponseToAirecall(String response , String username throw new RuntimeException("응답 매핑 중 오류 발생: " + e.getMessage()); } } - + private LocalDate convertStringToDate(String date) { + if (date != null) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: '{}'. 기본값을 사용합니다.", date); + return LocalDate.now(); // 기본값 설정 + } + } private String extractValue(String content, String key) { if (content == null || content.isEmpty()) { log.warn("내용이 비어 있음: {}", key); diff --git a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_kpt_AnalysisService.java b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_kpt_AnalysisService.java index 2b8e792d..29e3b6e1 100644 --- a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_kpt_AnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_kpt_AnalysisService.java @@ -12,6 +12,8 @@ import java.time.DayOfWeek; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -45,7 +47,8 @@ public void analyzeAirecall(List recallList, String u 3. improvement : 일기 내용에서 개선할 수 있는 부분이나 행동이나 상황을 개선할 수 있는 방법을 제안해 주세요. 4. scalability : 일기 내용에서 사용자의 활동이나 경험에서 확장 가능성이나 성장의 기회를 분석해 주세요. 5. **중요**절대 모든 값의 Null 및 0을 반환하지 마세요. 비슷한 분석 결과값을 반환해주세요. - 6. 위의 내용을 포함하여 각 항목을 객체 타입으로 한번만 반환해주세요. 예: + 6. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 7. 위의 내용을 포함하여 각 항목을 객체 타입으로 한번만 반환해주세요. 예: start_date: 2024-12-01 end_date: 2024-12-31 type : KTP @@ -78,9 +81,10 @@ private Airecall_Kpt_DTO mapResponseToAirecall(String response, String username) /** * 날짜 변경 */ - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); /** * type 변경 @@ -94,8 +98,8 @@ private Airecall_Kpt_DTO mapResponseToAirecall(String response, String username) return new Airecall_Kpt_DTO( airecallType, - startDate, - endDate, + start_date, + end_date, strength_analysis, improvement, scalability, @@ -107,6 +111,18 @@ private Airecall_Kpt_DTO mapResponseToAirecall(String response, String username) throw new RuntimeException("응답 매핑 중 오류 발생: " + e.getMessage()); } } + private LocalDate convertStringToDate(String date) { + if (date != null) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: '{}'. 기본값을 사용합니다.", date); + return LocalDate.now(); // 기본값 설정 + } + } private String extractValue(String content, String key) { if (content == null || content.isEmpty()) { log.warn("내용이 비어 있음: {}", key); diff --git a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_pmi_AnalysisService.java b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_pmi_AnalysisService.java index 27da2a27..7993ebe9 100644 --- a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_pmi_AnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_pmi_AnalysisService.java @@ -49,7 +49,8 @@ public void analyzeAirecall(List recallList , String 4. interesting : 일기 내용에서 흥미롭거나 주목할 만한 요소를 분석하고 설명해 주세요. 5. conclusion_action : 일기 내용에 대해 최종 결론을 내고, 실행 가능한 제안이나 개선 방안을 제시해 주세요. 6. **중요**절대 모든 값의 Null 및 0을 반환하지 마세요. 비슷한 분석 결과값을 반환해주세요. - 7. 위의 내용을 포함하여 각 항목을 객체 타입으로 한번만 반환해주세요. 예: + 7. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 8. 위의 내용을 포함하여 각 항목을 객체 타입으로 한번만 반환해주세요. 예: start_date: 2024-12-01 end_date: 2024-12-31 type : PMI @@ -83,9 +84,10 @@ private Airecall_Pmi_DTO mapResponseToAirecall(String response , String username /** * 날짜 변경 */ - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 + String startDate = extractValue(content, "start_date"); + String endDate = extractValue(content, "end_date"); + LocalDate start_date = convertStringToDate(startDate); + LocalDate end_date = convertStringToDate(endDate); /** * type 변경 @@ -100,8 +102,8 @@ private Airecall_Pmi_DTO mapResponseToAirecall(String response , String username return new Airecall_Pmi_DTO( airecallType, - startDate, - endDate, + start_date, + end_date, positive, minus, interesting, @@ -114,7 +116,18 @@ private Airecall_Pmi_DTO mapResponseToAirecall(String response , String username throw new RuntimeException("응답 매핑 중 오류 발생: " + e.getMessage()); } } - + private LocalDate convertStringToDate(String date) { + if (date != null) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: '{}'. 기본값을 사용합니다.", date); + return LocalDate.now(); // 기본값 설정 + } + } private String extractValue(String content, String key) { if (content == null || content.isEmpty()) { log.warn("내용이 비어 있음: {}", key); diff --git a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_scs_AnalysisService.java b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_scs_AnalysisService.java index 29d718f1..d19f8a8d 100644 --- a/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_scs_AnalysisService.java +++ b/src/main/java/com/woozuda/backend/ai_recall/service/AiRecall_scs_AnalysisService.java @@ -44,7 +44,8 @@ public void analyzeAirecall(List recallList , String 당신은 분석 도우미입니다. 사용자의 회고 데이터를 분석하고 다음과 같은 정보를 제공하세요: 1. type 이 "SCS" 인 경우 SCS 을 정확히 출력해주세요. 2.**중요**절대 모든 값의 Null 및 0을 반환하지 마세요. 비슷한 분석 결과값을 반환해주세요. - 3. 위의 내용을 포함하여 각 항목을 객체 타입으로 한번만 반환해주세요. 예: + 3. 시작날짜와 끝나는 날짜는 꼭 출력해주세요. + 4. 위의 내용을 포함하여 각 항목을 객체 타입으로 한번만 반환해주세요. 예: start_date: 2024-12-01 end_date: 2024-12-31 type: SCS @@ -86,9 +87,8 @@ private Airecll_Scs_DTO mapResponseToAirecall(String response , String username) /** * 날짜 변경 */ - LocalDate today = LocalDate.now(); - LocalDate startDate = today.with(DayOfWeek.MONDAY); // 이번 주 월요일 - LocalDate endDate = today.with(DayOfWeek.SUNDAY); // 이번 주 일요일 + LocalDate startDate = convertStringToDate("start_date"); + LocalDate endDate = convertStringToDate("end_date"); /** * type 변경 @@ -135,6 +135,9 @@ private Airecll_Scs_DTO mapResponseToAirecall(String response , String username) } private LocalDate convertStringToDate(String date) { + if (date != null) { + date = date.replaceAll("\"", ""); // 따옴표 제거 + } DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); try { return LocalDate.parse(date, formatter); diff --git a/src/main/java/com/woozuda/backend/alarm/controller/AlarmController.java b/src/main/java/com/woozuda/backend/alarm/controller/AlarmController.java new file mode 100644 index 00000000..7da05960 --- /dev/null +++ b/src/main/java/com/woozuda/backend/alarm/controller/AlarmController.java @@ -0,0 +1,35 @@ +package com.woozuda.backend.alarm.controller; + +import com.woozuda.backend.account.dto.CustomUser; +import com.woozuda.backend.alarm.service.AlarmService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RestController +@RequestMapping("/api/alarm") +@RequiredArgsConstructor +public class AlarmController { + + private final AlarmService alarmService; + + @GetMapping("/connect") + public SseEmitter subscribe(@AuthenticationPrincipal CustomUser customUser){ + String username = customUser.getUsername(); + return alarmService.connect(username); + } + + //프론트 테스트용 api - 매번 일기 3개 만들기가 쉽지 않을 것 같아서 제작. + @GetMapping("/connect/test") + public ResponseEntity alarmTest(@AuthenticationPrincipal CustomUser customUser){ + String username = customUser.getUsername(); + alarmService.alarmTest(username); + + return ResponseEntity.status(HttpStatus.OK).build(); + } +} diff --git a/src/main/java/com/woozuda/backend/alarm/service/AlarmService.java b/src/main/java/com/woozuda/backend/alarm/service/AlarmService.java new file mode 100644 index 00000000..2c694e95 --- /dev/null +++ b/src/main/java/com/woozuda/backend/alarm/service/AlarmService.java @@ -0,0 +1,134 @@ +package com.woozuda.backend.alarm.service; + +import com.woozuda.backend.shortlink.repository.SharedNoteRepoImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cglib.core.Local; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.WeekFields; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AlarmService { + + private final SharedNoteRepoImpl sharedNoteRepo; + + // 메모리 저장소 (서버 켤 때 마다 일관된 값을 들고 있는게 아니라, 연결 자체를 기록하는 것이므로 메모리 저장소도 괜찮음) + // 서버가 여러 대로 늘어나면 redis 도입을 해 봐야함 + // + private final Map emitters = new ConcurrentHashMap<>(); + + public SseEmitter connect(String username){ + + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + + emitters.put(username, emitter); + + //emitter 가 완료 되거나 타임아웃 되면 리스트에서 제거 + emitter.onCompletion(() -> { + log.info("emitter.onCompletion 발생"); + emitters.remove(username); + }); + + emitter.onTimeout(() -> { + log.info("emitter.onTimeout 발생"); + emitter.complete(); + emitters.remove(username); + }); + + emitter.onError((e) -> { + log.info("emitter.onError 발생 : {}", e.getMessage()); + emitters.remove(username); + }); + + // 초기 1회는 답을 해주어야 함 + try{ + emitter.send(SseEmitter.event() + .name("CONNECTED") + .data("SSE connect")); + }catch(IOException e){ + log.info("초기 연결 메세지 못보냈음"); + emitter.completeWithError(e); + emitters.remove(username); + } + + return emitter; + } + + public void threePostAlarm(String username, String date){ + Long postCount = sharedNoteRepo.noteCountToMakeReport(username, date); + log.info("postCount 는 {}", postCount); + + if(postCount == 3){ + SseEmitter emitter = emitters.get(username); + + if(emitter == null){ + return; + } + + WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY, 1); + LocalDate nowDate = LocalDate.parse(date); + int week = nowDate.get(weekFields.weekOfMonth()); + int month = nowDate.getMonthValue(); + int year = nowDate.getYear(); + + String msg = String.format("%d년 %d월 %d주 AI 레포트를 생성해보세요!", year, month, week); + + try{ + emitter.send(SseEmitter.event() + .name("message") + .data(msg)); + }catch(IOException e){ + log.info("알람 메세지 못보냈음"); + emitters.remove(username); + } + } + } + + + public void alarmTest(String username){ + SseEmitter emitter = emitters.get(username); + + if(emitter == null){ + return; + } + + try{ + emitter.send(SseEmitter.event() + .name("message") + .data("테스트 알람 메세지 입니다.")); + }catch(IOException e){ + log.info("테스트 알람 메세지 못보냈음"); + emitters.remove(username); + } + } + + @Scheduled(fixedDelay = 21600000) // 360분(6시간)마다 하트비트 + public void sendHeartbeat() { + + //log.info("하트비트 확인용 "); + for (Map.Entry entry : emitters.entrySet()) { + + SseEmitter emitter = entry.getValue(); + + try { + emitter.send(SseEmitter.event() + .name("ping") + .data("heartbeat")); + } catch (IOException e) { + log.info("emitter 핑 실패"); + } + }; + } +} diff --git a/src/main/java/com/woozuda/backend/aop/LogExecutionTime.java b/src/main/java/com/woozuda/backend/aop/LogExecutionTime.java new file mode 100644 index 00000000..6f7e50ec --- /dev/null +++ b/src/main/java/com/woozuda/backend/aop/LogExecutionTime.java @@ -0,0 +1,11 @@ +package com.woozuda.backend.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LogExecutionTime { +} diff --git a/src/main/java/com/woozuda/backend/aop/LogExecutionTimeAspect.java b/src/main/java/com/woozuda/backend/aop/LogExecutionTimeAspect.java new file mode 100644 index 00000000..ae8ed877 --- /dev/null +++ b/src/main/java/com/woozuda/backend/aop/LogExecutionTimeAspect.java @@ -0,0 +1,25 @@ +package com.woozuda.backend.aop; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Slf4j +public class LogExecutionTimeAspect { + + @Around("@annotation(com.woozuda.backend.aop.LogExecutionTime)") + public void logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + + // 실제 메서드 실행 + joinPoint.proceed(); + + long executionTime = System.currentTimeMillis() - start; + log.info("[LogExecutionTime] {} executed in {} ms", + joinPoint.getSignature(), executionTime); + } +} diff --git a/src/main/java/com/woozuda/backend/diary/service/DiaryService.java b/src/main/java/com/woozuda/backend/diary/service/DiaryService.java index 2cf25543..58675e64 100644 --- a/src/main/java/com/woozuda/backend/diary/service/DiaryService.java +++ b/src/main/java/com/woozuda/backend/diary/service/DiaryService.java @@ -11,6 +11,8 @@ import com.woozuda.backend.diary.dto.response.SingleDiaryResponseDto; import com.woozuda.backend.diary.entity.Diary; import com.woozuda.backend.diary.repository.DiaryRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.image.type.ImageType; import com.woozuda.backend.note.dto.request.NoteCondRequestDto; import com.woozuda.backend.note.dto.response.NoteEntryResponseDto; import com.woozuda.backend.note.dto.response.NoteResponseDto; @@ -40,6 +42,7 @@ public class DiaryService { private final UserRepository userRepository; private final TagRepository tagRepository; private final NoteRepository noteRepository; + private final ImageService imageService; @Transactional(readOnly = true) public DiaryListResponseDto getDairyList(String username) { @@ -100,6 +103,10 @@ public DiaryIdResponseDto saveDiary(String username, DiarySaveRequestDto request } Diary savedDiary = diaryRepository.save(diary); + + // 이미지 테이블 변경(다이어리 생성 시) + imageService.afterCreate(ImageType.DIARY, savedDiary.getId(), requestDto.getImgUrl()); + return new DiaryIdResponseDto(savedDiary.getId()); } @@ -122,6 +129,9 @@ public DiaryIdResponseDto updateDiary(String username, Long diaryId, DiarySaveRe diary.change(requestDto.getTitle(), tags, requestDto.getImgUrl()); } + // 이미지 테이블 반영 (다이어리 변경 시) + imageService.afterUpdate(ImageType.DIARY, diaryId, requestDto.getImgUrl()); + return new DiaryIdResponseDto(foundDiary.get().getId()); } @@ -134,6 +144,9 @@ public void removeDiary(String username, Long diaryId) { throw new IllegalArgumentException("This diary does not belong to the user."); } + // 이미지 테이블 반영 (다이어리 삭제 시) + imageService.afterDelete(ImageType.DIARY, diaryId); + diaryRepository.deleteById(diaryId); } diff --git a/src/main/java/com/woozuda/backend/exception/account/AccountExceptionHandler.java b/src/main/java/com/woozuda/backend/exception/account/AccountExceptionHandler.java new file mode 100644 index 00000000..a7e0574e --- /dev/null +++ b/src/main/java/com/woozuda/backend/exception/account/AccountExceptionHandler.java @@ -0,0 +1,22 @@ +package com.woozuda.backend.exception.account; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class AccountExceptionHandler { + + @ExceptionHandler(InvalidEmailException.class) + public ResponseEntity handleInvalidEmailException(InvalidEmailException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + + @ExceptionHandler(UsernameAlreadyExistsException.class) + public ResponseEntity handleUsernameAlreadyExistsException(UsernameAlreadyExistsException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } +} diff --git a/src/main/java/com/woozuda/backend/exception/InvalidEmailException.java b/src/main/java/com/woozuda/backend/exception/account/InvalidEmailException.java similarity index 75% rename from src/main/java/com/woozuda/backend/exception/InvalidEmailException.java rename to src/main/java/com/woozuda/backend/exception/account/InvalidEmailException.java index 0b82e6b1..91e16d1a 100644 --- a/src/main/java/com/woozuda/backend/exception/InvalidEmailException.java +++ b/src/main/java/com/woozuda/backend/exception/account/InvalidEmailException.java @@ -1,4 +1,4 @@ -package com.woozuda.backend.exception; +package com.woozuda.backend.exception.account; public class InvalidEmailException extends RuntimeException{ public InvalidEmailException(String message) { diff --git a/src/main/java/com/woozuda/backend/exception/UsernameAlreadyExistsException.java b/src/main/java/com/woozuda/backend/exception/account/UsernameAlreadyExistsException.java similarity index 77% rename from src/main/java/com/woozuda/backend/exception/UsernameAlreadyExistsException.java rename to src/main/java/com/woozuda/backend/exception/account/UsernameAlreadyExistsException.java index ff254b57..5fc93749 100644 --- a/src/main/java/com/woozuda/backend/exception/UsernameAlreadyExistsException.java +++ b/src/main/java/com/woozuda/backend/exception/account/UsernameAlreadyExistsException.java @@ -1,4 +1,4 @@ -package com.woozuda.backend.exception; +package com.woozuda.backend.exception.account; public class UsernameAlreadyExistsException extends RuntimeException { public UsernameAlreadyExistsException(String message) { diff --git a/src/main/java/com/woozuda/backend/forai/dto/CountRecallDto.java b/src/main/java/com/woozuda/backend/forai/dto/CountRecallDto.java deleted file mode 100644 index b947c124..00000000 --- a/src/main/java/com/woozuda/backend/forai/dto/CountRecallDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.woozuda.backend.forai.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class CountRecallDto { - private long ffs; - private long kpt; - private long pmi; - private long scs; - @Override - public String toString() { - return "CountRecallDto{" + - "ffs=" + ffs + - ", kpt=" + kpt + - ", pmi=" + pmi + - ", scs=" + scs + - '}'; - } -} diff --git a/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAi.java b/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAi.java index 54a16d8e..4b821f30 100644 --- a/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAi.java +++ b/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAi.java @@ -1,6 +1,5 @@ package com.woozuda.backend.forai.repository; -import com.woozuda.backend.forai.dto.CountRecallDto; import com.woozuda.backend.forai.dto.NonRetroNoteEntryResponseDto; import com.woozuda.backend.forai.dto.RetroNoteEntryResponseDto; import com.woozuda.backend.note.entity.type.Framework; diff --git a/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImpl.java b/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImpl.java index 12d57b2a..c73ffa30 100644 --- a/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImpl.java +++ b/src/main/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImpl.java @@ -2,10 +2,8 @@ import com.querydsl.core.Tuple; import com.querydsl.core.types.Projections; -import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; -import com.woozuda.backend.forai.dto.CountRecallDto; import com.woozuda.backend.forai.dto.NonRetroNoteEntryResponseDto; import com.woozuda.backend.forai.dto.RetroNoteEntryResponseDto; import com.woozuda.backend.note.entity.type.Framework; diff --git a/src/main/java/com/woozuda/backend/forai/service/CustomeNoteRepoForAiService.java b/src/main/java/com/woozuda/backend/forai/service/CustomeNoteRepoForAiService.java index 742ac418..06abe7cb 100644 --- a/src/main/java/com/woozuda/backend/forai/service/CustomeNoteRepoForAiService.java +++ b/src/main/java/com/woozuda/backend/forai/service/CustomeNoteRepoForAiService.java @@ -2,7 +2,6 @@ import com.woozuda.backend.account.entity.UserEntity; import com.woozuda.backend.account.repository.UserRepository; -import com.woozuda.backend.forai.dto.CountRecallDto; import com.woozuda.backend.forai.repository.CustomNoteRepoForAi; import com.woozuda.backend.forai.dto.NonRetroNoteEntryResponseDto; import com.woozuda.backend.forai.dto.RetroNoteEntryResponseDto; diff --git a/src/main/java/com/woozuda/backend/global/config/OpenFeignConfig.java b/src/main/java/com/woozuda/backend/global/config/OpenFeignConfig.java new file mode 100644 index 00000000..d9395e10 --- /dev/null +++ b/src/main/java/com/woozuda/backend/global/config/OpenFeignConfig.java @@ -0,0 +1,9 @@ +package com.woozuda.backend.global.config; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients("com.woozuda.backend.question.service") +public class OpenFeignConfig { +} diff --git a/src/main/java/com/woozuda/backend/global/config/SchedulerConfig.java b/src/main/java/com/woozuda/backend/global/config/SchedulerConfig.java new file mode 100644 index 00000000..12fd5291 --- /dev/null +++ b/src/main/java/com/woozuda/backend/global/config/SchedulerConfig.java @@ -0,0 +1,28 @@ +package com.woozuda.backend.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * Spring Scheduler 는 기본적으로 싱글 스레드도 동작. + * 그러나 이 애플리케이션에서 사용하는 스케줄러는 현재 두 개 + * : AiQuestionCreationService.makeTodayAiQuestion(), AlarmService.sendHeartbeat() + * 확률이 매우 낮긴 하지만 이 두 스케줄러가 동시에 실행될 때, 싱글 스레드 환경이라면 예상과는 다르게 동작할 수도 있음. + * 따라서 스케줄링 작업에 할당할 스레드를 3개로 지정해, 하나의 스케줄러가 다른 것에 영향을 미치치 않도록 멀티 스레드 환경을 구축 + */ +@Configuration +public class SchedulerConfig implements SchedulingConfigurer { + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + ThreadPoolTaskScheduler threadPool = new ThreadPoolTaskScheduler(); + + threadPool.setPoolSize(3); + threadPool.setThreadNamePrefix("scheduler-task"); + threadPool.initialize(); + + taskRegistrar.setTaskScheduler(threadPool); + } +} diff --git a/src/main/java/com/woozuda/backend/image/controller/ImageController.java b/src/main/java/com/woozuda/backend/image/controller/ImageController.java index 5c87db9a..1523fff7 100644 --- a/src/main/java/com/woozuda/backend/image/controller/ImageController.java +++ b/src/main/java/com/woozuda/backend/image/controller/ImageController.java @@ -13,6 +13,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.List; @RequestMapping("/api/image") @RequiredArgsConstructor @@ -28,6 +29,14 @@ public ResponseEntity uploadImage(MultipartFile multipartFile) throws return ResponseEntity.ok(responseDto); } + /* + @PostMapping("/delete") + public void deleteImage(){ + String url = "https://kr.object.ncloudstorage.com/woozuda-image/test-dummy.png"; + imageService.deleteImage(url.split("/")[4]); + } + */ + // 랜덤 이미지 추출 @GetMapping("/random") public ResponseEntity getRandomImage(){ diff --git a/src/main/java/com/woozuda/backend/image/cron/ImageDeleteTask.java b/src/main/java/com/woozuda/backend/image/cron/ImageDeleteTask.java new file mode 100644 index 00000000..ceb19233 --- /dev/null +++ b/src/main/java/com/woozuda/backend/image/cron/ImageDeleteTask.java @@ -0,0 +1,36 @@ +package com.woozuda.backend.image.cron; + +import com.woozuda.backend.image.entity.Image; +import com.woozuda.backend.image.repository.ImageRepository; +import com.woozuda.backend.image.service.ImageService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class ImageDeleteTask { + + private final ImageService imageService; + private final ImageRepository imageRepository; + + @Scheduled(cron = "0 0 3 * * *") // 매일 오전 3시 + public void cleanUpImages() { + // 게시물과 연결되지 않은 이미지 조회 + List deleteImages = imageRepository.findByIsLinkedToPost(false); + List deleteImageIds = new ArrayList<>(); + + // 해당 이미지 object storage 에서 삭제 + for(Image deleteImage : deleteImages){ + // 지울 이미지들의 Id 추가 (db 에도 반영 위해서) + deleteImageIds.add(deleteImage.getId()); + imageService.deleteImage(deleteImage.getImageUrl().split("/")[4]); + } + + // db(이미지 테이블) 도 그에 맞추어 삭제 + imageRepository.deleteAllById(deleteImageIds); + } +} diff --git a/src/main/java/com/woozuda/backend/image/entity/Image.java b/src/main/java/com/woozuda/backend/image/entity/Image.java index 1777a3f9..48bc9049 100644 --- a/src/main/java/com/woozuda/backend/image/entity/Image.java +++ b/src/main/java/com/woozuda/backend/image/entity/Image.java @@ -2,11 +2,10 @@ import com.woozuda.backend.global.entity.BaseTimeEntity; +import com.woozuda.backend.image.type.ImageType; import jakarta.persistence.*; import lombok.Getter; -import lombok.Setter; -@Setter @Getter @Entity @Table(name="image") @@ -23,7 +22,13 @@ public class Image extends BaseTimeEntity { @Column(name = "is_linked_to_post") private Boolean isLinkedToPost; - public Image(String imageUrl, Boolean isLinkedToPost) { + @Column(name = "image_type") + private ImageType imageType; + + @Column(name = "connected_id") + private Long connectedId; + + private Image(String imageUrl, Boolean isLinkedToPost) { this.imageUrl = imageUrl; this.isLinkedToPost = isLinkedToPost; } @@ -31,4 +36,16 @@ public Image(String imageUrl, Boolean isLinkedToPost) { public static Image of(String imageUrl, Boolean isLinkedToPost) { return new Image(imageUrl, isLinkedToPost); } + + public void changeLinkedToPost(Boolean isLinkedToPost) { + this.isLinkedToPost = isLinkedToPost; + } + + public void changeImageType(ImageType imageType){ + this.imageType = imageType; + } + + public void changeConectedId(Long connectedId){ + this.connectedId = connectedId; + } } diff --git a/src/main/java/com/woozuda/backend/image/repository/ImageRepository.java b/src/main/java/com/woozuda/backend/image/repository/ImageRepository.java index 6b14370c..7ac97cc5 100644 --- a/src/main/java/com/woozuda/backend/image/repository/ImageRepository.java +++ b/src/main/java/com/woozuda/backend/image/repository/ImageRepository.java @@ -1,7 +1,21 @@ package com.woozuda.backend.image.repository; import com.woozuda.backend.image.entity.Image; +import com.woozuda.backend.image.type.ImageType; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ImageRepository extends JpaRepository { + + // ImageType 과 ConnectedId 가 일치하는 이미지를 찾는 쿼리 + public List findByImageTypeAndConnectedId(ImageType imageType, Long connectedId); + + // imageUrl이 일치하는 이미지를 찾는 쿼리 (select * from image where image_url IN (image_url1 , image_url2 , ....) + public List findByImageUrlIn(List imageUrls); + + // isLinkedToPost 가 true/false 인 이미지 찾기. + public List findByIsLinkedToPost(Boolean isLinkedToPost); + + public Image findByImageUrl(String imageUrl); } diff --git a/src/main/java/com/woozuda/backend/image/service/ImageService.java b/src/main/java/com/woozuda/backend/image/service/ImageService.java index 7135f908..ff79ad9d 100644 --- a/src/main/java/com/woozuda/backend/image/service/ImageService.java +++ b/src/main/java/com/woozuda/backend/image/service/ImageService.java @@ -10,24 +10,34 @@ import com.woozuda.backend.image.dto.ImageDto; import com.woozuda.backend.image.entity.Image; import com.woozuda.backend.image.repository.ImageRepository; +import com.woozuda.backend.image.type.ImageType; +import jakarta.persistence.Column; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Random; +import java.util.UUID; @Service @RequiredArgsConstructor +@Transactional +@Slf4j public class ImageService { private final S3Client s3Client; private final ImageRepository imageRepository; - - @Transactional public ImageDto uploadImage(MultipartFile multipartFile) throws IOException { // 올릴 이미지가 첨부되지 않았을 경우 @@ -39,6 +49,9 @@ public ImageDto uploadImage(MultipartFile multipartFile) throws IOException { // 파일 이름 추출 String filename = multipartFile.getOriginalFilename(); + String uuid = UUID.randomUUID().toString(); + filename = uuid + "_" +filename; + String bucketname = s3Client.getBucketName(); // 메타데이터 생성 @@ -62,7 +75,109 @@ public ImageDto uploadImage(MultipartFile multipartFile) throws IOException { return new ImageDto(imgUrl); } - @Transactional + public void deleteImage(String filename){ + AmazonS3 s3 = s3Client.getAmazonS3(); + s3.deleteObject(s3Client.getBucketName(), filename); + } + + // urlContent 를 가공해주는 메서드 + public List makeImgsUrl(ImageType imageType, String urlContent){ + + List imgs = new ArrayList<>(); + + if(imageType == ImageType.NOTE){ + // Note는 content가

안녕하세요

excludeBasicImage(List imageStrings){ + imageStrings.removeIf(image -> image.startsWith("https://kr.object.ncloudstorage.com/woozuda-image/random-image")); + return imageStrings; + } + + public void afterCreate(ImageType imageType, Long connectedId, String content){ + //content에서 imageUrl 추출( 리스트로 변환) + List imagesUrl = makeImgsUrl(imageType, content); + + //기본 이미지 - 주어지는 default 이미지는 제외 (필수는 아니나 혹시 해서 넣어둔 로직) + imagesUrl = excludeBasicImage(imagesUrl); + + List images = imageRepository.findByImageUrlIn(imagesUrl); + + // 영속 상태의 엔티티라 save() 하지 않아도 반영됨 + for(Image image : images){ + image.changeLinkedToPost(true); + image.changeImageType(imageType); + image.changeConectedId(connectedId); + } + } + + public void afterUpdate(ImageType imageType, Long connectedId, String content){ + + List imagesUrl = makeImgsUrl(imageType, content); + + //기본 이미지 - 주어지는 default 이미지는 제외 (필수는 아니나 혹시 해서 넣어둔 로직) + imagesUrl = excludeBasicImage(imagesUrl); + + // 이번에 업데이트할 글에 저장되어있는 그림들 리스트 + List afterImages = imageRepository.findByImageUrlIn(imagesUrl); + boolean[] afterImagesBool = new boolean[afterImages.size()]; + + //기존에 글(다이어리)에 저장되어있던 그림들을 불러옴 + List beforeImages = imageRepository.findByImageTypeAndConnectedId(imageType, connectedId); + boolean[] beforeImagesBool = new boolean[beforeImages.size()]; + + //수정 전과 수정 후에 둘다 있는 이미지는 true 처리 - 그리고 이 이미지들은 수정할 필요 없음. + for(int i=0; i < beforeImages.size(); i++){ + for(int j=0; j < afterImages.size(); j++) { + if (beforeImages.get(i).getImageUrl().equals(afterImages.get(j).getImageUrl())) { + beforeImagesBool[i] = true; + afterImagesBool[j] = true; + } + } + } + + // 수정 전 이미지 리스트 중 false 인 것들은 이번에 수정되면서 삭제된 이미지 임 - 고로 삭제 처리 해줘야 함 + List deleteImagesId = new ArrayList<>(); + for(int i=0; i < beforeImages.size(); i++){ + if(!beforeImagesBool[i]){ + deleteImagesId.add(beforeImages.get(i).getId()); + } + } + imageRepository.deleteAllById(deleteImagesId); + + // 수정 후 이미지 리스트 중 false 인 것들은 이번에 수정되면서 추가된 이미지 임 - 고로 추가 처리 해줘야 함 + for(int j=0; j < afterImages.size(); j++){ + if(!afterImagesBool[j]){ + Image nowImage = afterImages.get(j); + + nowImage.changeLinkedToPost(true); + nowImage.changeImageType(imageType); + nowImage.changeConectedId(connectedId); + } + } + + } + + public void afterDelete(ImageType imageType, Long connectedId){ + List images = imageRepository.findByImageTypeAndConnectedId(imageType, connectedId); + imageRepository.deleteAll(images); + } + + @Transactional(readOnly = true) public ImageDto getRandomImage() { Random random = new Random(); int imageNumber = random.nextInt(10) + 1; diff --git a/src/main/java/com/woozuda/backend/image/type/ImageType.java b/src/main/java/com/woozuda/backend/image/type/ImageType.java new file mode 100644 index 00000000..60a236ab --- /dev/null +++ b/src/main/java/com/woozuda/backend/image/type/ImageType.java @@ -0,0 +1,6 @@ +package com.woozuda.backend.image.type; + +public enum ImageType { + DIARY, + NOTE +} diff --git a/src/main/java/com/woozuda/backend/note/entity/NoteContent.java b/src/main/java/com/woozuda/backend/note/entity/NoteContent.java index 7abb69d1..d5d6ce85 100644 --- a/src/main/java/com/woozuda/backend/note/entity/NoteContent.java +++ b/src/main/java/com/woozuda/backend/note/entity/NoteContent.java @@ -1,7 +1,9 @@ package com.woozuda.backend.note.entity; import com.woozuda.backend.global.entity.BaseTimeEntity; +import com.woozuda.backend.note.entity.converter.NoteContentConverter; import jakarta.persistence.Column; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -34,6 +36,7 @@ public class NoteContent extends BaseTimeEntity { private Integer noteOrder; @Column(length = 2000, nullable = false) + @Convert(converter = NoteContentConverter.class) private String content; // 회고 부분 내용 private NoteContent(Integer noteOrder, String content) { diff --git a/src/main/java/com/woozuda/backend/note/entity/QuestionNote.java b/src/main/java/com/woozuda/backend/note/entity/QuestionNote.java index 57eb8963..5f576830 100644 --- a/src/main/java/com/woozuda/backend/note/entity/QuestionNote.java +++ b/src/main/java/com/woozuda/backend/note/entity/QuestionNote.java @@ -5,6 +5,7 @@ import com.woozuda.backend.note.entity.type.Season; import com.woozuda.backend.note.entity.type.Visibility; import com.woozuda.backend.note.entity.type.Weather; +import com.woozuda.backend.question.entity.Question; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; diff --git a/src/main/java/com/woozuda/backend/note/entity/converter/AesEncryptor.java b/src/main/java/com/woozuda/backend/note/entity/converter/AesEncryptor.java new file mode 100644 index 00000000..299757fb --- /dev/null +++ b/src/main/java/com/woozuda/backend/note/entity/converter/AesEncryptor.java @@ -0,0 +1,50 @@ +package com.woozuda.backend.note.entity.converter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class AesEncryptor { + + public static final String SALT_DATA_DELIMITER = "::"; + + private final String password; + + public AesEncryptor(@Value("${aes.password}") String password) { + this.password = password; + } + + public String encrypt(String plainText) { + //무작위 salt 생성 + String salt = KeyGenerators.string().generateKey(); + TextEncryptor encryptor = Encryptors.delux(password, salt); + + // salt를 암호화된 데이터와 함께 저장 + return salt + SALT_DATA_DELIMITER + encryptor.encrypt(plainText); + } + + public String decrypt(String encryptedText) { + // 암호화된 데이터에서 salt 분리 + String[] parts = encryptedText.split(SALT_DATA_DELIMITER, 2); + if (parts.length > 2) { + throw new IllegalArgumentException("Invalid encrypted text format"); + } + if (parts.length == 1) { + return encryptedText; + } + + String salt = parts[0]; + String cipherText = parts[1]; + + TextEncryptor encryptor = Encryptors.delux(password, salt); + + return encryptor.decrypt(cipherText); + } + + +} diff --git a/src/main/java/com/woozuda/backend/note/entity/converter/NoteContentConverter.java b/src/main/java/com/woozuda/backend/note/entity/converter/NoteContentConverter.java new file mode 100644 index 00000000..170a2cb2 --- /dev/null +++ b/src/main/java/com/woozuda/backend/note/entity/converter/NoteContentConverter.java @@ -0,0 +1,38 @@ +package com.woozuda.backend.note.entity.converter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.RequiredArgsConstructor; + +@Converter +@RequiredArgsConstructor +public class NoteContentConverter implements AttributeConverter { + + private final AesEncryptor aesEncryptor; + + @Override + public String convertToDatabaseColumn(String attribute) { + if (attribute == null) { + return null; + } + + try { + return aesEncryptor.encrypt(attribute); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + try { + return aesEncryptor.decrypt(dbData); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/woozuda/backend/note/repository/CustomNoteRepositoryImpl.java b/src/main/java/com/woozuda/backend/note/repository/CustomNoteRepositoryImpl.java index d3d88710..2c8c7daf 100644 --- a/src/main/java/com/woozuda/backend/note/repository/CustomNoteRepositoryImpl.java +++ b/src/main/java/com/woozuda/backend/note/repository/CustomNoteRepositoryImpl.java @@ -26,9 +26,9 @@ import static com.woozuda.backend.note.entity.QCommonNote.commonNote; import static com.woozuda.backend.note.entity.QNote.note; import static com.woozuda.backend.note.entity.QNoteContent.noteContent; -import static com.woozuda.backend.note.entity.QQuestion.question; import static com.woozuda.backend.note.entity.QQuestionNote.questionNote; import static com.woozuda.backend.note.entity.QRetrospectiveNote.retrospectiveNote; +import static com.woozuda.backend.question.entity.QQuestion.question; import static java.lang.Long.sum; /** diff --git a/src/main/java/com/woozuda/backend/note/service/CommonNoteService.java b/src/main/java/com/woozuda/backend/note/service/CommonNoteService.java index b58d1a40..ede8cba0 100644 --- a/src/main/java/com/woozuda/backend/note/service/CommonNoteService.java +++ b/src/main/java/com/woozuda/backend/note/service/CommonNoteService.java @@ -1,8 +1,11 @@ package com.woozuda.backend.note.service; +import com.woozuda.backend.alarm.service.AlarmService; import com.woozuda.backend.diary.dto.response.NoteIdResponseDto; import com.woozuda.backend.diary.entity.Diary; import com.woozuda.backend.diary.repository.DiaryRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.image.type.ImageType; import com.woozuda.backend.note.dto.request.CommonNoteModifyRequestDto; import com.woozuda.backend.note.dto.request.CommonNoteSaveRequestDto; import com.woozuda.backend.note.dto.response.NoteResponseDto; @@ -31,6 +34,8 @@ public class CommonNoteService { private final CommonNoteRepository commonNoteRepository; private final NoteRepository noteRepository; private final DiaryRepository diaryRepository; + private final AlarmService alarmService; + private final ImageService imageService; public NoteIdResponseDto saveCommonNote(String username, CommonNoteSaveRequestDto requestDto) { Diary foundDiary = diaryRepository.searchDiary(requestDto.getDiaryId(), username); @@ -52,6 +57,12 @@ public NoteIdResponseDto saveCommonNote(String username, CommonNoteSaveRequestDt foundDiary.addNote(savedCommonNote.getDate()); + // 이번에 저장한 자유일기가 그 주의 3번째 일기라면(자유일기 + 질문일기 기준), 알람을 발생합니다. + alarmService.threePostAlarm(username, requestDto.getDate()); + + // 이미지 테이블 반영 (자유일기 생성 후) + imageService.afterCreate(ImageType.NOTE, savedCommonNote.getId(), requestDto.getContent()); + return NoteIdResponseDto.of(savedCommonNote.getId()); } @@ -80,6 +91,9 @@ public NoteIdResponseDto updateCommonNote(String username, Long noteId, CommonNo requestDto.getContent() ); + // 이미지 테이블 반영 (자유일기 변경 후) + imageService.afterUpdate(ImageType.NOTE, noteId, requestDto.getContent()); + return NoteIdResponseDto.of(foundNote.getId()); } } diff --git a/src/main/java/com/woozuda/backend/note/service/NoteService.java b/src/main/java/com/woozuda/backend/note/service/NoteService.java index 9112939e..ec7ce0f6 100644 --- a/src/main/java/com/woozuda/backend/note/service/NoteService.java +++ b/src/main/java/com/woozuda/backend/note/service/NoteService.java @@ -2,6 +2,8 @@ import com.woozuda.backend.diary.entity.Diary; import com.woozuda.backend.diary.repository.DiaryRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.image.type.ImageType; import com.woozuda.backend.note.dto.request.NoteCondRequestDto; import com.woozuda.backend.note.dto.request.NoteIdRequestDto; import com.woozuda.backend.note.dto.response.DateInfoResponseDto; @@ -33,6 +35,7 @@ public class NoteService { private final NoteRepository noteRepository; private final DiaryRepository diaryRepository; + private final ImageService imageService; /** * 최신순 일기 조회 @@ -105,6 +108,15 @@ public void deleteNotes(String username, NoteIdRequestDto requestDto) { for (Diary diary : diariesToChange) { diary.updateNoteInfo(requestDto.getId()); } + + + //해당 노트에 써있던 이미지들 삭제 + List deleteNoteIds = requestDto.getId(); + + for(Long deleteNoteId : deleteNoteIds){ + imageService.afterDelete(ImageType.NOTE, deleteNoteId); + } + } public NoteCountResponseDto getNoteCount(String username, LocalDate startDate, LocalDate endDate) { diff --git a/src/main/java/com/woozuda/backend/note/service/QuestionNoteService.java b/src/main/java/com/woozuda/backend/note/service/QuestionNoteService.java index ebeab07b..595538fd 100644 --- a/src/main/java/com/woozuda/backend/note/service/QuestionNoteService.java +++ b/src/main/java/com/woozuda/backend/note/service/QuestionNoteService.java @@ -1,21 +1,23 @@ package com.woozuda.backend.note.service; +import com.woozuda.backend.alarm.service.AlarmService; import com.woozuda.backend.diary.dto.response.NoteIdResponseDto; import com.woozuda.backend.diary.entity.Diary; import com.woozuda.backend.diary.repository.DiaryRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.image.type.ImageType; import com.woozuda.backend.note.dto.request.QuestionNoteModifyRequestDto; import com.woozuda.backend.note.dto.request.QuestionNoteSaveRequestDto; import com.woozuda.backend.note.dto.response.NoteResponseDto; -import com.woozuda.backend.note.entity.CommonNote; import com.woozuda.backend.note.entity.NoteContent; -import com.woozuda.backend.note.entity.Question; +import com.woozuda.backend.question.entity.Question; import com.woozuda.backend.note.entity.QuestionNote; import com.woozuda.backend.note.entity.type.Feeling; import com.woozuda.backend.note.entity.type.Season; import com.woozuda.backend.note.entity.type.Weather; import com.woozuda.backend.note.repository.NoteRepository; import com.woozuda.backend.note.repository.QuestionNoteRepository; -import com.woozuda.backend.note.repository.QuestionRepository; +import com.woozuda.backend.question.repository.QuestionRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +36,8 @@ public class QuestionNoteService { private final NoteRepository noteRepository; private final DiaryRepository diaryRepository; private final QuestionRepository questionRepository; + private final AlarmService alarmService; + private final ImageService imageService; public NoteIdResponseDto saveQuestionNote(String username, QuestionNoteSaveRequestDto requestDto) { Diary foundDiary = diaryRepository.searchDiary(requestDto.getDiaryId(), username); @@ -59,6 +63,12 @@ public NoteIdResponseDto saveQuestionNote(String username, QuestionNoteSaveReque foundDiary.addNote(savedQuestionNote.getDate()); + // 이번에 저장한 질문일기가 그 주의 3번째 일기라면(자유일기 + 질문일기 기준), 알람을 발생합니다. + alarmService.threePostAlarm(username, requestDto.getDate()); + + // 이미지 테이블 반영 (질문 일기 생성 후) + imageService.afterCreate(ImageType.NOTE, savedQuestionNote.getId(), requestDto.getContent()); + return NoteIdResponseDto.of(savedQuestionNote.getId()); } @@ -86,6 +96,9 @@ public NoteIdResponseDto updateQuestionNote(String username, Long noteId, Questi requestDto.getContent() ); + // 이미지 테이블 반영 (질문 일기 변경 후) + imageService.afterUpdate(ImageType.NOTE, noteId, requestDto.getContent()); + return NoteIdResponseDto.of(foundNote.getId()); } } diff --git a/src/main/java/com/woozuda/backend/note/controller/QuestionController.java b/src/main/java/com/woozuda/backend/question/controller/QuestionController.java similarity index 81% rename from src/main/java/com/woozuda/backend/note/controller/QuestionController.java rename to src/main/java/com/woozuda/backend/question/controller/QuestionController.java index 4b00a33a..186288f6 100644 --- a/src/main/java/com/woozuda/backend/note/controller/QuestionController.java +++ b/src/main/java/com/woozuda/backend/question/controller/QuestionController.java @@ -1,7 +1,7 @@ -package com.woozuda.backend.note.controller; +package com.woozuda.backend.question.controller; -import com.woozuda.backend.note.dto.response.QuestionResponseDto; -import com.woozuda.backend.note.service.QuestionService; +import com.woozuda.backend.question.dto.response.QuestionResponseDto; +import com.woozuda.backend.question.service.QuestionService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; diff --git a/src/main/java/com/woozuda/backend/note/dto/response/QuestionResponseDto.java b/src/main/java/com/woozuda/backend/question/dto/response/QuestionResponseDto.java similarity index 76% rename from src/main/java/com/woozuda/backend/note/dto/response/QuestionResponseDto.java rename to src/main/java/com/woozuda/backend/question/dto/response/QuestionResponseDto.java index fa7e2d62..bfc5f174 100644 --- a/src/main/java/com/woozuda/backend/note/dto/response/QuestionResponseDto.java +++ b/src/main/java/com/woozuda/backend/question/dto/response/QuestionResponseDto.java @@ -1,6 +1,6 @@ -package com.woozuda.backend.note.dto.response; +package com.woozuda.backend.question.dto.response; -import com.woozuda.backend.note.entity.Question; +import com.woozuda.backend.question.entity.Question; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -15,4 +15,5 @@ public class QuestionResponseDto { public static QuestionResponseDto from(Question question) { return new QuestionResponseDto(question.getContent()); } + } diff --git a/src/main/java/com/woozuda/backend/note/entity/Question.java b/src/main/java/com/woozuda/backend/question/entity/Question.java similarity index 94% rename from src/main/java/com/woozuda/backend/note/entity/Question.java rename to src/main/java/com/woozuda/backend/question/entity/Question.java index c76f0da4..528289ef 100644 --- a/src/main/java/com/woozuda/backend/note/entity/Question.java +++ b/src/main/java/com/woozuda/backend/question/entity/Question.java @@ -1,4 +1,4 @@ -package com.woozuda.backend.note.entity; +package com.woozuda.backend.question.entity; import com.woozuda.backend.global.entity.BaseTimeEntity; import jakarta.persistence.Column; diff --git a/src/main/java/com/woozuda/backend/note/repository/QuestionRepository.java b/src/main/java/com/woozuda/backend/question/repository/QuestionRepository.java similarity index 80% rename from src/main/java/com/woozuda/backend/note/repository/QuestionRepository.java rename to src/main/java/com/woozuda/backend/question/repository/QuestionRepository.java index 8e3d2494..f34c3690 100644 --- a/src/main/java/com/woozuda/backend/note/repository/QuestionRepository.java +++ b/src/main/java/com/woozuda/backend/question/repository/QuestionRepository.java @@ -1,6 +1,6 @@ -package com.woozuda.backend.note.repository; +package com.woozuda.backend.question.repository; -import com.woozuda.backend.note.entity.Question; +import com.woozuda.backend.question.entity.Question; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/src/main/java/com/woozuda/backend/question/service/AiQuestionCreationService.java b/src/main/java/com/woozuda/backend/question/service/AiQuestionCreationService.java new file mode 100644 index 00000000..c03e4661 --- /dev/null +++ b/src/main/java/com/woozuda/backend/question/service/AiQuestionCreationService.java @@ -0,0 +1,60 @@ +package com.woozuda.backend.question.service; + +import com.woozuda.backend.question.entity.Question; +import com.woozuda.backend.question.repository.QuestionRepository; +import com.woozuda.backend.question.service.dto.request.AiQuestionRequestDto; +import com.woozuda.backend.question.service.dto.response.AiQuestionResponseDto; +import com.woozuda.backend.question.service.util.AiInputGenerator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class AiQuestionCreationService { + + private final QuestionRepository questionRepository; + private final AiQuestionCreatorApiClient apiClient; + + @Value("${ncp.clova-studio.question-creator.api-key}") + private String apiKey; + + @Value("${ncp.clova-studio.question-creator.apigw-key}") + private String apigwKey; + + // 매일 자정 12시 00분 1초에 새로운 질문 생성 + @Scheduled(cron = "1 0 0 * * *") + public void makeTodayAiQuestion() { + AiQuestionRequestDto requestDto = AiQuestionRequestDto.of(AiInputGenerator.execute()); + log.info("[AI Question Creator] input={}", requestDto.getText()); + + //AI 질문 생성기 API 호출 + AiQuestionResponseDto responseDto = apiClient.makeAiQuestion( + apiKey, + apigwKey, + requestDto + ); + + if (hasError(responseDto)) { + throw new IllegalArgumentException("API 요청에 실패했습니다"); + } + + String output = responseDto.getResult().getText(); + log.info("[AI Question Creator] output={}", output); + + //생성된 질문 저장 + questionRepository.save(Question.of(output)); + } + + //응답 상태 코드가 20000번대(성공)가 아니면 false 반환 + private boolean hasError(AiQuestionResponseDto response) { + String code = response.getStatus().getCode(); + return !code.equals("20000") && !code.equals("20400"); + } + +} diff --git a/src/main/java/com/woozuda/backend/question/service/AiQuestionCreatorApiClient.java b/src/main/java/com/woozuda/backend/question/service/AiQuestionCreatorApiClient.java new file mode 100644 index 00000000..c7be971e --- /dev/null +++ b/src/main/java/com/woozuda/backend/question/service/AiQuestionCreatorApiClient.java @@ -0,0 +1,22 @@ +package com.woozuda.backend.question.service; + +import com.woozuda.backend.question.service.dto.request.AiQuestionRequestDto; +import com.woozuda.backend.question.service.dto.response.AiQuestionResponseDto; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@FeignClient(name = "AiQuestionCreatorApiClient", url = "${ncp.clova-studio.question-creator.url}") +public interface AiQuestionCreatorApiClient { + + @PostMapping(consumes = APPLICATION_JSON_VALUE) + AiQuestionResponseDto makeAiQuestion( + @RequestHeader("X-NCP-CLOVASTUDIO-API-KEY") String apiKey, + @RequestHeader("X-NCP-APIGW-API-KEY") String apigwKey, + @RequestBody AiQuestionRequestDto requestDto + ); + +} diff --git a/src/main/java/com/woozuda/backend/note/service/QuestionService.java b/src/main/java/com/woozuda/backend/question/service/QuestionService.java similarity index 76% rename from src/main/java/com/woozuda/backend/note/service/QuestionService.java rename to src/main/java/com/woozuda/backend/question/service/QuestionService.java index 06acbf7a..81ac5473 100644 --- a/src/main/java/com/woozuda/backend/note/service/QuestionService.java +++ b/src/main/java/com/woozuda/backend/question/service/QuestionService.java @@ -1,8 +1,8 @@ -package com.woozuda.backend.note.service; +package com.woozuda.backend.question.service; -import com.woozuda.backend.note.dto.response.QuestionResponseDto; -import com.woozuda.backend.note.entity.Question; -import com.woozuda.backend.note.repository.QuestionRepository; +import com.woozuda.backend.question.dto.response.QuestionResponseDto; +import com.woozuda.backend.question.entity.Question; +import com.woozuda.backend.question.repository.QuestionRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/woozuda/backend/question/service/dto/request/AiQuestionRequestDto.java b/src/main/java/com/woozuda/backend/question/service/dto/request/AiQuestionRequestDto.java new file mode 100644 index 00000000..bd885fc4 --- /dev/null +++ b/src/main/java/com/woozuda/backend/question/service/dto/request/AiQuestionRequestDto.java @@ -0,0 +1,32 @@ +package com.woozuda.backend.question.service.dto.request; + +import lombok.Getter; + +import java.util.Collections; +import java.util.List; + +@Getter +public class AiQuestionRequestDto { + + private final String text; + + private final Integer topK = 0; + private final Float topP = 0.80f; + private final Float repeatPenalty = 6.0f; + private final List stopBefore = Collections.emptyList(); + private final String restart = ""; + private final String start = ""; + private final Integer maxTokens = 128; + private final Boolean includeTokens = true; + private final Float temperature = 0.6f; + private final Boolean includeAiFilters = false; + + public AiQuestionRequestDto(String text) { + this.text = text; + } + + public static AiQuestionRequestDto of(String text) { + return new AiQuestionRequestDto(text); + } + +} diff --git a/src/main/java/com/woozuda/backend/question/service/dto/response/AiQuestionResponseDto.java b/src/main/java/com/woozuda/backend/question/service/dto/response/AiQuestionResponseDto.java new file mode 100644 index 00000000..9b2c3ed1 --- /dev/null +++ b/src/main/java/com/woozuda/backend/question/service/dto/response/AiQuestionResponseDto.java @@ -0,0 +1,52 @@ +package com.woozuda.backend.question.service.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class AiQuestionResponseDto { + + private ApiStatus status; + private ApiResult result; + + @AllArgsConstructor + @NoArgsConstructor + @Getter + public static class ApiStatus { + private String code; + private String message; + } + + @AllArgsConstructor + @NoArgsConstructor + @Getter + public static class ApiResult { + private String text; + private String stopReason; + private Integer startLength; + private String inputText; + private Integer inputLength; + private List inputTokens; + private String outputText; + private Integer outputLength; + private List outputTokens; + private List probs; + private Boolean ok; + private List aiFilter; + } + + @AllArgsConstructor + @NoArgsConstructor + @Getter + public static class AiFilter { + private String groupName; + private String name; + private String score; + } + +} diff --git a/src/main/java/com/woozuda/backend/question/service/util/AiInputGenerator.java b/src/main/java/com/woozuda/backend/question/service/util/AiInputGenerator.java new file mode 100644 index 00000000..b45f0c6e --- /dev/null +++ b/src/main/java/com/woozuda/backend/question/service/util/AiInputGenerator.java @@ -0,0 +1,24 @@ +package com.woozuda.backend.question.service.util; + +import java.util.List; +import java.util.Random; + +public class AiInputGenerator { + + private final static List keywords = List.of( + "소중한 순간", "성장", "변화", "결정", "친절", "자아성찰", "노력", "한계", "가치관", "관점", "목표", + "전환점", "용기", "도전", "관계", "배려", "습관", "열정", "선택", "용서", "계획", "창의성", "성공", + "시간 관리", "시작", "행복", "우정", "인내심", "신념", "독서", "집중", "실패 경험", "긍정", + "감사함", "존중", "자기계발", "추억", "배움", "휴식", "꿈", "경험", "영향력", "소통", "결심", "성취", + "자유", "신뢰", "가족", "책임감", "사랑" + ); + + private final static String INPUT_SUFFIX = "에 대한 질문을 작성해줘."; + + //keywords 기반 랜덤 질문 생성 + public static String execute() { + int randomIndex = new Random().nextInt(keywords.size()); + return keywords.get(randomIndex) + INPUT_SUFFIX; + } + +} diff --git a/src/main/java/com/woozuda/backend/security/config/SecurityConfig.java b/src/main/java/com/woozuda/backend/security/config/SecurityConfig.java index c9e516b0..de112a98 100644 --- a/src/main/java/com/woozuda/backend/security/config/SecurityConfig.java +++ b/src/main/java/com/woozuda/backend/security/config/SecurityConfig.java @@ -1,6 +1,7 @@ package com.woozuda.backend.security.config; import com.woozuda.backend.account.service.CustomOAuth2UserService; +import com.woozuda.backend.security.jwt.IPCheckFilter; import com.woozuda.backend.security.jwt.JWTFilter; import com.woozuda.backend.security.jwt.JWTUtil; import com.woozuda.backend.security.jwt.LoginFilter; @@ -8,8 +9,10 @@ import com.woozuda.backend.security.oauth2.CustomSuccessHandler; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -28,6 +31,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.List; @RequiredArgsConstructor @Configuration @@ -46,6 +50,9 @@ public class SecurityConfig { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + @Value("${allow-ips}") + private List adminIps; + //AuthenticationManager Bean 등록 @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { @@ -59,9 +66,8 @@ public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - + // 필터 공통 설정 + private void configureCommon(HttpSecurity http) throws Exception { //csrf, formlogin, httpbasic 필터를 비활성화 http .csrf((auth) -> auth.disable()) @@ -75,39 +81,65 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .userService(customOAuth2UserService)) .successHandler(customSuccessHandler) ); + http + .exceptionHandling(handling -> handling + .authenticationEntryPoint(customAuthenticationEntryPoint)); + + //stateless 세션 설정 + http + .sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http + .cors(cors -> cors + .configurationSource(corsConfigurationSource())); + + } + @Bean + @Order(1) + public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception { + + configureCommon(http); + + // /account/sample/admin 에 해당되는 api만 해당 filter chain 이 돌도록 함 + http + .securityMatcher("/account/sample/admin"); //경로별 인가 작업 http .authorizeHttpRequests((auth) -> auth - .requestMatchers("/login", "/", "/join", "/error", "/account/sample/alluser", "/favicon.ico", "/api/shortlink/note/**", "/api/shortlink/ai/**", "/actuator/**").permitAll() .requestMatchers("/account/sample/admin").hasRole("ADMIN") .anyRequest().authenticated()); - http - .exceptionHandling(handling -> handling - .authenticationEntryPoint(customAuthenticationEntryPoint)); - //UserNamePasswordAuthenticationFilter 자리에 커스텀 하게 만든 LoginFilter를 실행한다. //jwt 방식으로 구현하다 보니 , form login 을 비활성화했고, UserNamePasswordAuthenticationFilter 도 비활성화 되었음 (그래서 커스텀 구현이 필요) http - //.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class) - //.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class) - //.addFilterAfter(new JWTFilter(jwtUtil), BasicAuthenticationFilter.class) - //.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), BasicAuthenticationFilter.class); .addFilterAfter(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class) - .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class); - //.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class) - //.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), OAuth2LoginAuthenticationFilter.class); + .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new IPCheckFilter(adminIps), JWTFilter.class); - //stateless 세션 설정 + return http.build(); + + } + + @Bean + @Order(2) + public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception { + + configureCommon(http); + + //경로별 인가 작업 http - .sessionManagement((session) -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/login", "/", "/join", "/error", "/account/sample/alluser", "/favicon.ico", "/api/shortlink/note/**", "/api/shortlink/ai/**", "/actuator/**").permitAll() + .anyRequest().authenticated()); + //UserNamePasswordAuthenticationFilter 자리에 커스텀 하게 만든 LoginFilter를 실행한다. + //jwt 방식으로 구현하다 보니 , form login 을 비활성화했고, UserNamePasswordAuthenticationFilter 도 비활성화 되었음 (그래서 커스텀 구현이 필요) http - .cors(cors -> cors - .configurationSource(corsConfigurationSource())); + .addFilterAfter(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class) + .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class); return http.build(); diff --git a/src/main/java/com/woozuda/backend/security/jwt/IPCheckFilter.java b/src/main/java/com/woozuda/backend/security/jwt/IPCheckFilter.java new file mode 100644 index 00000000..d56d9cd1 --- /dev/null +++ b/src/main/java/com/woozuda/backend/security/jwt/IPCheckFilter.java @@ -0,0 +1,51 @@ +package com.woozuda.backend.security.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +public class IPCheckFilter extends OncePerRequestFilter { + + private final List adminIps; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + String remoteAddr = getClientIpAddr(request); + + if (!adminIps.contains(remoteAddr)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "IP not allowed"); + return; + } + + filterChain.doFilter(request, response); + } + + public static String getClientIpAddr(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } +} diff --git a/src/main/java/com/woozuda/backend/security/jwt/JWTFilter.java b/src/main/java/com/woozuda/backend/security/jwt/JWTFilter.java index 682b458d..aea3732f 100644 --- a/src/main/java/com/woozuda/backend/security/jwt/JWTFilter.java +++ b/src/main/java/com/woozuda/backend/security/jwt/JWTFilter.java @@ -26,16 +26,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String path = request.getRequestURI(); - // 필터를 적용하지 않을 경로를 지정 - if (path.equals("/join") || path.equals("/login") || path.equals("/account/sample/alluser") || path.startsWith("/api/shortlink/note") || path.startsWith("/api/shortlink/ai") || path.startsWith("/actuator")) { - filterChain.doFilter(request, response); - return; - } - - //Authorization 을 키 값으로 가지는 것을 찾음 - // 헤더 버전 - //String authorization= request.getHeader("Authorization"); + // String authorization = request.getHeader("Authorization"); String authorization = null; diff --git a/src/main/java/com/woozuda/backend/security/oauth2/CustomSuccessHandler.java b/src/main/java/com/woozuda/backend/security/oauth2/CustomSuccessHandler.java index 0c2359e0..3e0ffdd6 100644 --- a/src/main/java/com/woozuda/backend/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/com/woozuda/backend/security/oauth2/CustomSuccessHandler.java @@ -41,7 +41,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo ResponseCookie responseCookie = createCookie("Authorization", token); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - response.sendRedirect("https://woozuda.swygbro.com/home"); + response.sendRedirect("https://woozuda.swygbro.com"); //response.addCookie(createCookie("Authorization", token)); } diff --git a/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepo.java b/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepo.java index 5cf48595..3d560c33 100644 --- a/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepo.java +++ b/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepo.java @@ -14,4 +14,5 @@ public interface SharedNoteRepo { List searchNoteContent(Note note); + Long noteCountToMakeReport(String username, String date); } diff --git a/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepoImpl.java b/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepoImpl.java index 50bda40a..b284565d 100644 --- a/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepoImpl.java +++ b/src/main/java/com/woozuda/backend/shortlink/repository/SharedNoteRepoImpl.java @@ -3,7 +3,9 @@ import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; +import com.woozuda.backend.note.entity.CommonNote; import com.woozuda.backend.note.entity.Note; +import com.woozuda.backend.note.entity.QuestionNote; import com.woozuda.backend.note.entity.type.Visibility; import com.woozuda.backend.shortlink.dto.note.SharedCommonNoteDto; import com.woozuda.backend.shortlink.dto.note.SharedNoteDto; @@ -11,12 +13,14 @@ import com.woozuda.backend.shortlink.dto.note.SharedRetrospectiveNoteDto; import jakarta.persistence.EntityManager; +import java.time.*; import java.util.List; import static com.querydsl.core.group.GroupBy.list; import static com.querydsl.core.group.GroupBy.groupBy; import static com.woozuda.backend.account.entity.QUserEntity.userEntity; import static com.woozuda.backend.diary.entity.QDiary.diary; import static com.woozuda.backend.note.entity.QCommonNote.commonNote; +import static com.woozuda.backend.note.entity.QNote.note; import static com.woozuda.backend.note.entity.QNoteContent.noteContent; import static com.woozuda.backend.note.entity.QQuestionNote.questionNote; import static com.woozuda.backend.note.entity.QRetrospectiveNote.retrospectiveNote; @@ -94,6 +98,25 @@ public List searchSharedRetrospectiveNote(String username) { )); } + // 특정 유저의 질문일기, 자유일기 수를 카운트 하는 쿼리 . (알람 기능에 쓰입니다) + @Override + public Long noteCountToMakeReport(String username, String date){ + + //저장 노트의 월 ~ 일 범위 자르기 (월요일 00:00 ~ 그 다음주 월요일 00:00) + LocalDate saveDate = LocalDate.parse(date); + LocalDate thisWeekStart = saveDate.with(DayOfWeek.MONDAY); + LocalDate thisWeekEnd = thisWeekStart.plusDays(6); + + return query + .select(note.count()) + .from(note) + .leftJoin(note.diary, diary) + .leftJoin(diary.user, userEntity) + .where(userEntity.username.eq(username) + .and(note.instanceOf(CommonNote.class).or(note.instanceOf(QuestionNote.class))) + .and(note.date.between(thisWeekStart, thisWeekEnd))) + .fetchOne(); + } @Override diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 8346803a..2427a0f9 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -68,15 +68,29 @@ ncp: accessKey: "${object-storage.accessKey}" secretKey: "${object-storage.secretKey}" bucketName : "${object-storage.bucketName}" + clova-studio: + question-creator: + url: "${questionCreator.url}" + api-key: "${questionCreator.apiKey}" + apigw-key: "${questionCreator.apigwKey}" openai: api: key: "${openai.api.key}" # 환경 변수에서 OPENAI_API_KEY를 가져옵니다. + url: "${openai.api.url}" chat: options: model: gpt-3.5-turbo # 사용할 AI 모델 (예: gpt-3.5-turbo) temperature: 0.7 # 생성되는 텍스트의 다양성 정도를 조절하는 파라미터 (0.0 ~ 1.0) +clova: + api: + key: "${clova.api.key}" + url: "${clova.api.url}" + rid: "${clova.api.rid}" + + + management: server: port: "${actuator.port}" @@ -87,3 +101,9 @@ management: endpoint: prometheus: enabled: true #default + +allow-ips: "${allow_ips}" + +aes: + password: "${aes-password}" + diff --git a/src/main/resources/application-release.yml b/src/main/resources/application-release.yml index ab120c4a..62a564b3 100644 --- a/src/main/resources/application-release.yml +++ b/src/main/resources/application-release.yml @@ -64,10 +64,16 @@ ncp: accessKey: "${object_storage_access_key}" secretKey: "${object_storage_secret_key}" bucketName: "${object_storage_bucket_name}" + clova-studio: + question-creator: + url: "${question_creator_url}" + api-key: "${question_creator_api_key}" + apigw-key: "${question_creator_apigw_key}" openai: api: key: "${openai_api_key}" # 환경 변수에서 OPENAI_API_KEY를 가져옵니다. + url: "${openai.api.url}" chat: options: model: gpt-3.5-turbo # 사용할 AI 모델 (예: gpt-3.5-turbo) @@ -85,4 +91,9 @@ management: enabled: true #default logging: - config: "classpath:./logback-release.xml" \ No newline at end of file + config: "classpath:./logback-release.xml" + +allow-ips: "${allow_ips}" + +aes: + password: "${aes_password}" diff --git a/src/test/java/com/woozuda/backend/account/service/JoinServiceUnitTest.java b/src/test/java/com/woozuda/backend/account/service/JoinServiceUnitTest.java new file mode 100644 index 00000000..68b47cc0 --- /dev/null +++ b/src/test/java/com/woozuda/backend/account/service/JoinServiceUnitTest.java @@ -0,0 +1,106 @@ +package com.woozuda.backend.account.service; + + +import com.woozuda.backend.account.dto.JoinDTO; +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.exception.account.InvalidEmailException; +import com.woozuda.backend.exception.account.UsernameAlreadyExistsException; +import com.woozuda.backend.shortlink.util.ShortLinkUtil; +import com.woozuda.backend.testutil.UserEntityBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +// 회원가입 단위 테스트. +@ExtendWith(MockitoExtension.class) +public class JoinServiceUnitTest { + + @InjectMocks + JoinService joinService; + + @Mock + UserRepository userRepository; + + @Mock + BCryptPasswordEncoder bCryptPasswordEncoder; + + @Mock + ShortLinkUtil shortLinkUtil; + + @Test + void 회원가입_성공(){ + + //given + UserEntity userEntity = UserEntityBuilder.createUniqueUser().build(); + + // userRepository 에 UserEntity 를 넣었을 때 userEntity 를 반환하는 것으로 가정. 이미 다른 곳에서 검증 된 기능이라고 가정 한다. + when(userRepository.save(any(UserEntity.class))).thenReturn(userEntity); + when(bCryptPasswordEncoder.encode(userEntity.getPassword())).thenReturn("!@#$"); + doNothing().when(shortLinkUtil).saveShortLink(any(UserEntity.class)); + + //when (joinService.joinProcess 실험) + JoinDTO joinDTO = joinService.joinProcess(new JoinDTO(userEntity.getUsername(), userEntity.getPassword())); + + //then + assertEquals(userEntity.getUsername(), joinDTO.getUsername()); + assertEquals(userEntity.getPassword(), joinDTO.getPassword()); + + verify(userRepository, times(1)).existsByUsername(userEntity.getUsername()); + verify(userRepository, times(1)).save(any(UserEntity.class)); + } + + @Test + void 회원가입_실패_중복유저(){ + + //given + UserEntity userEntity = UserEntityBuilder.createUniqueUser().build(); + + // userRepository 에 UserEntity 를 넣었을 때 userEntity 를 반환하는 것으로 가정. 이미 다른 곳에서 검증 된 기능이라고 가정 한다. + when(userRepository.existsByUsername(userEntity.getUsername())).thenReturn(true); + + //when (joinService.joinProcess 실험) + Exception exception = assertThrows(UsernameAlreadyExistsException.class, () -> { + JoinDTO joinDTO = joinService.joinProcess(new JoinDTO(userEntity.getUsername(), userEntity.getPassword())); + }); + + //then + // 특정 예외가 발생하는지 검증 + String expectedMessage = "이미 존재하는 회원 입니다"; + String actualMessage = exception.getMessage(); + assertTrue(exception.getMessage().contains(expectedMessage)); + + //save 호출 되면 안됨 + verify(userRepository, times(0)).save(any(UserEntity.class)); + } + + @Test + void 회원가입_실패_이메일형태틀림(){ + + //given + UserEntity userEntity = UserEntityBuilder.createUniqueUser().withUsername("test").build(); + + //when (joinService.joinProcess 실험) + Exception exception = assertThrows(InvalidEmailException.class, () -> { + JoinDTO joinDTO = joinService.joinProcess(new JoinDTO(userEntity.getUsername(), userEntity.getPassword())); + }); + + //then + // 특정 예외가 발생하는지 검증 + String expectedMessage = "잘못된 이메일 형식을 입력 했습니다"; + String actualMessage = exception.getMessage(); + assertTrue(exception.getMessage().contains(expectedMessage)); + + //save 호출 되면 안됨 + verify(userRepository, times(0)).save(any(UserEntity.class)); + } + +} diff --git a/src/test/java/com/woozuda/backend/account/service/JoinTest.java b/src/test/java/com/woozuda/backend/account/service/JoinTest.java new file mode 100644 index 00000000..0d303c59 --- /dev/null +++ b/src/test/java/com/woozuda/backend/account/service/JoinTest.java @@ -0,0 +1,84 @@ +package com.woozuda.backend.account.service; + +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.security.jwt.JWTUtil; +import com.woozuda.backend.shortlink.repository.ShortLinkRepository; +import com.woozuda.backend.testutil.UserEntityBuilder; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +// 회원가입 통합 테스트. +@SpringBootTest +@AutoConfigureMockMvc +public class JoinTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ShortLinkRepository shortLinkRepository; + + @BeforeEach + void setUp(){ + UserEntityBuilder.resetCounter(); + shortLinkRepository.deleteAll(); + userRepository.deleteAll(); + userRepository.save(UserEntityBuilder.createUniqueUser().build()); + } + + @Test + public void 회원가입_성공() throws Exception { + + //given + String requestBody = "{\"username\":\"user2@gmail.com\", \"password\":\"1234\"}"; + + //when + mockMvc.perform(post("/join") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isOk()); + } + + @Test + public void 회원가입_실패_중복유저() throws Exception { + + //given + String requestBody = "{\"username\":\"user1@gmail.com\", \"password\":\"1234\"}"; + + //when + mockMvc.perform(post("/join") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isConflict()); + } + + @Test + public void 회원가입_실패_잘못된형식() throws Exception { + + //given + String requestBody = "{\"username\":\"user1\", \"password\":\"1234\"}"; + + //when + mockMvc.perform(post("/join") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isBadRequest()); + } + + +} diff --git a/src/test/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryTest.java b/src/test/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryTest.java new file mode 100644 index 00000000..35d4bd67 --- /dev/null +++ b/src/test/java/com/woozuda/backend/ai_creation/repository/AiCreationRepositoryTest.java @@ -0,0 +1,78 @@ +package com.woozuda.backend.ai_creation.repository; + +import com.woozuda.backend.account.entity.AiType; +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.ai_creation.dto.AiCreationDTO; +import com.woozuda.backend.ai_creation.entity.AiCreation; +import com.woozuda.backend.ai_creation.entity.CreationType; +import com.woozuda.backend.ai_creation.entity.CreationVisibility; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +class AiCreationRepositoryTest { + @Autowired + private AiCreationRepository aiCreationRepository; + + @Autowired + private UserRepository userRepository; + + UserEntity user; + AiCreation aiCreation; + + @BeforeEach + void setUp() { + user = new UserEntity(null, "sun", "sun0304@", "ROLE_USER", AiType.PICTURE_NOVEL, true, "sun@gmail.com", "woozuda"); + + // 실패가 떠야함 + //user = new UserEntity(null, "sun", "sun0304@", "ROLE_USER", AiType.PICTURE_POETRY, true, "sun@gmail.com", "woozuda"); + userRepository.save(user); + + // AiType이 PICTURE_NOVEL일 때만 CreationType을 설정 + AiCreationDTO aiCreationDTO; + if (user.getAiType() == AiType.PICTURE_NOVEL) { + + aiCreationDTO = new AiCreationDTO( + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + CreationType.POETRY.name(), // POETRY일 때 통과 + "image_url", + "text", + CreationVisibility.PUBLIC.name(), + user.getUsername() + ); + + aiCreation = AiCreation.toCreationEntity(aiCreationDTO, user); + aiCreationRepository.save(aiCreation); + } + } + + @DisplayName("AI 창작 분석") + @Test + void AICreation() { + // 주어진 아이디로 AI 창작을 찾기 + AiCreation found = aiCreationRepository.findById(aiCreation.getAi_creation_id()) + .orElseThrow(() -> new RuntimeException("AI 창작 데이터를 찾을 수 없습니다.")); + + // 검증: 저장된 엔티티가 null이 아님을 확인 + assertNotNull(found, "저장된 AI 창작 데이터가 null입니다."); + + // 저장된 AI 창작 데이터가 예상대로 저장되었는지 검증 + assertEquals(aiCreation.getCreationType(), found.getCreationType(), "Creation type이 일치하지 않습니다."); + assertEquals(aiCreation.getImage_url(), found.getImage_url(), "Image URL이 일치하지 않습니다."); + assertEquals(aiCreation.getText(), found.getText(), "Text가 일치하지 않습니다."); + assertEquals(aiCreation.getCreationVisibility(), found.getCreationVisibility(), "Visibility가 일치하지 않습니다."); + assertEquals(user.getUsername(), found.getUser().getUsername(), "User가 일치하지 않습니다."); + } + +} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryTest.java b/src/test/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryTest.java new file mode 100644 index 00000000..b887c40d --- /dev/null +++ b/src/test/java/com/woozuda/backend/ai_diary/repository/AiDiaryRepositoryTest.java @@ -0,0 +1,80 @@ +package com.woozuda.backend.ai_diary.repository; + + +import com.woozuda.backend.account.entity.AiType; +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.ai_diary.dto.AiDiaryDTO; +import com.woozuda.backend.ai_diary.entity.AiDiary; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +class AiDiaryRepositoryTest { + @Autowired + private AiDiaryRepository aiDiaryRepository; + + @Autowired + private UserRepository userRepository; + + + UserEntity user; + AiDiary aiDiary; + + @BeforeEach + void setUp() { + // User 엔티티 준비 및 저장 + user = new UserEntity(null, "sun", "sun0304@", "ROLE_USER", AiType.PICTURE_NOVEL, true, "sun@gmail.com", "woozuda"); + userRepository.save(user); + + // AiDiaryDTO 객체 생성 + AiDiaryDTO aiDiaryDTO = new AiDiaryDTO( + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + "도서관", + "독서", + "기쁨", + "맑음", + 0.6f, + 0.4f, + 0.8f, + 0.2f, + "더 자주 독서 시간을 가져보세요!", + user.getUsername() + ); + + // AiDiaryDTO를 AiDiary 엔티티로 변환 + aiDiary = AiDiary.toEntity(aiDiaryDTO, user); + + aiDiary = aiDiaryRepository.save(aiDiary); + } + + @DisplayName("AI 분석 결과 보기") + @Test + void AIDiary() { + // when + AiDiary result = aiDiaryRepository.findByAiDiary(aiDiary.getStart_date(),aiDiary.getEnd_date() , aiDiary.getUser().getUsername()) + .orElseThrow(() -> new RuntimeException("AiDiary not found")); + + // then + assertNotNull(result, "AI 분석 결과는 null이어서는 안됩니다."); + + assertEquals(aiDiary.getPlace(), result.getPlace(), "장소가 일치하지 않습니다."); + assertEquals(aiDiary.getEmotion(), result.getEmotion(), "감정이 일치하지 않습니다."); + assertEquals(aiDiary.getWeather(), result.getWeather(), "날씨가 일치하지 않습니다."); + assertEquals(aiDiary.getWeekdayAt(), result.getWeekdayAt(), 0.001f, "평일 비율이 일치하지 않습니다."); + assertEquals(aiDiary.getWeekendAt(), result.getWeekendAt(), 0.001f, "주말 비율이 일치하지 않습니다."); + assertEquals(aiDiary.getPositive(), result.getPositive(), 0.001f, "긍정 비율이 일치하지 않습니다."); + assertEquals(aiDiary.getDenial(), result.getDenial(), 0.001f, "부정 비율이 일치하지 않습니다."); + assertEquals(aiDiary.getSuggestion(), result.getSuggestion(), "추천 내용이 일치하지 않습니다."); + assertEquals(aiDiary.getUser().getUsername(), result.getUser().getUsername(), "사용자명이 일치하지 않습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/ai_diary/service/AiDiaryServiceTest.java b/src/test/java/com/woozuda/backend/ai_diary/service/AiDiaryServiceTest.java deleted file mode 100644 index af10f5b3..00000000 --- a/src/test/java/com/woozuda/backend/ai_diary/service/AiDiaryServiceTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.woozuda.backend.ai_diary.service; - - -import com.woozuda.backend.ai_diary.repository.AiDiaryRepository; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - - - - - -@SpringBootTest -@Transactional -class AiDiaryServiceTest { - private static final Logger logger = LoggerFactory.getLogger(AiDiaryServiceTest.class); - - @Autowired - private AiDiaryService aiDiaryService; - - @Autowired - private AiDiaryRepository aiDiaryRepository; - - //@Test -} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/ai_recall/repository/fourfs/AiRecall4fsRpositoryTest.java b/src/test/java/com/woozuda/backend/ai_recall/repository/fourfs/AiRecall4fsRpositoryTest.java new file mode 100644 index 00000000..1cb58c27 --- /dev/null +++ b/src/test/java/com/woozuda/backend/ai_recall/repository/fourfs/AiRecall4fsRpositoryTest.java @@ -0,0 +1,69 @@ +package com.woozuda.backend.ai_recall.repository.fourfs; + +import com.woozuda.backend.account.entity.AiType; +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.ai_recall.dto.Airecall_4fs_DTO; +import com.woozuda.backend.ai_recall.entity.Airecall_4fs; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest +@Transactional +class AiRecall4fsRpositoryTest { + @Autowired + private AiRecall4fsRpository aiRecall4fsRpository; + + @Autowired + private UserRepository userRepository; + + + UserEntity user; + Airecall_4fs airecall_4fs; + + @BeforeEach + void setUp() { + // User 엔티티 준비 및 저장 + user = new UserEntity(null, "sun", "sun0304@", "ROLE_USER", AiType.PICTURE_NOVEL, true, "sun@gmail.com", "woozuda"); + userRepository.save(user); + + // AiDiaryDTO 객체 생성 + Airecall_4fs_DTO airecall_4fs_dto = new Airecall_4fs_DTO( + "ffs", + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + "패턴 분석 내용", // patternAnalysis + "긍정적인 행동 예시", // positiveBehavior + "개선 제안 내용", // improvementSuggest + "활용 팁 내용", // utilizationTips + user.getUsername() + ); + + // AiDiaryDTO를 AiDiary 엔티티로 변환 + airecall_4fs = Airecall_4fs.toairecall4fsEntity(airecall_4fs_dto, user); + + airecall_4fs = aiRecall4fsRpository.save(airecall_4fs); + } + + @DisplayName("AI 4fs분석 결과 보기") + @Test + void FFS(){ + Airecall_4fs found = aiRecall4fsRpository.findById(airecall_4fs.getAir_id()).orElseThrow(() -> new RuntimeException("데이터를 찾을 수 없습니다.")); + + // 저장된 값들과 일치하는지 assertEquals로 검증 + assertNotNull(found, "저장된 데이터가 null입니다."); + assertEquals(airecall_4fs.getStart_date(), found.getStart_date(), "시작 날짜가 일치하지 않습니다."); + assertEquals(airecall_4fs.getEnd_date(), found.getEnd_date(), "끝 날짜가 일치하지 않습니다."); + assertEquals(airecall_4fs.getPatternAnalysis(), found.getPatternAnalysis(), "패턴 분석 내용이 일치하지 않습니다."); + assertEquals(airecall_4fs.getPositiveBehavior(), found.getPositiveBehavior(), "긍정적인 행동 예시가 일치하지 않습니다."); + assertEquals(airecall_4fs.getImprovementSuggest(), found.getImprovementSuggest(), "개선 제안 내용이 일치하지 않습니다."); + assertEquals(airecall_4fs.getUtilizationTips(), found.getUtilizationTips(), "활용 팁 내용이 일치하지 않습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/ai_recall/repository/kpt/AiRecallkptRpositoryTest.java b/src/test/java/com/woozuda/backend/ai_recall/repository/kpt/AiRecallkptRpositoryTest.java new file mode 100644 index 00000000..69679b86 --- /dev/null +++ b/src/test/java/com/woozuda/backend/ai_recall/repository/kpt/AiRecallkptRpositoryTest.java @@ -0,0 +1,64 @@ +package com.woozuda.backend.ai_recall.repository.kpt; + +import com.woozuda.backend.account.entity.AiType; +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.ai_recall.dto.Airecall_Kpt_DTO; +import com.woozuda.backend.ai_recall.entity.Airecall_kpt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest +@Transactional +class AiRecallkptRpositoryTest { + @Autowired + private AiRecallkptRpository aiRecallkptRpository; + + @Autowired + private UserRepository userRepository; + + + UserEntity user; + Airecall_kpt airecall_kpt; + + @BeforeEach + void setUp() { + user = new UserEntity(null, "sun", "sun0304@", "ROLE_USER", AiType.PICTURE_NOVEL, true, "sun@gmail.com", "woozuda"); + userRepository.save(user); + + // AiDiaryDTO 객체 생성 + Airecall_Kpt_DTO airecall_kpt_dto = new Airecall_Kpt_DTO( + "kpt", + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + "강점 분석", + "개선 제안", + "개선 제안 내용", + user.getUsername() + ); + + // AiDiaryDTO를 AiDiary 엔티티로 변환 + airecall_kpt = Airecall_kpt.toairecallktpEntity(airecall_kpt_dto, user); + + airecall_kpt = aiRecallkptRpository.save(airecall_kpt); + } + + @DisplayName("AI KPT 분석 결과 보기") + @Test + void kpt(){ + Airecall_kpt found = aiRecallkptRpository.findById(airecall_kpt.getAir_id()).orElseThrow(() -> new RuntimeException("데이터를 찾을 수 없습니다.")); + + // 저장된 값들과 일치하는지 assertEquals로 검증 + assertNotNull(found, "저장된 데이터가 null입니다."); + assertEquals(airecall_kpt.getStart_date(), found.getStart_date(), "시작 날짜가 일치하지 않습니다."); + assertEquals(airecall_kpt.getEnd_date(), found.getEnd_date(), "끝 날짜가 일치하지 않습니다."); + + } +} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/ai_recall/repository/scs/AiRecallscsRpositoryTest.java b/src/test/java/com/woozuda/backend/ai_recall/repository/scs/AiRecallscsRpositoryTest.java new file mode 100644 index 00000000..ae8053ad --- /dev/null +++ b/src/test/java/com/woozuda/backend/ai_recall/repository/scs/AiRecallscsRpositoryTest.java @@ -0,0 +1,77 @@ +package com.woozuda.backend.ai_recall.repository.scs; + +import com.woozuda.backend.account.entity.AiType; +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.ai_recall.dto.Airecll_Scs_DTO; +import com.woozuda.backend.ai_recall.entity.Airecall_scs; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest +@Transactional +class AiRecallscsRpositoryTest { + @Autowired + private UserRepository userRepository; + + @Autowired + private AiRecallscsRpository aiRecallscsRpository; + + + UserEntity user; + Airecall_scs airecall_scs; + + + @BeforeEach + void setUp() { + // User 엔티티 준비 및 저장 + user = new UserEntity(null, "sun", "sun0304@", "ROLE_USER", AiType.PICTURE_NOVEL, true, "sun@gmail.com", "woozuda"); + userRepository.save(user); + + // AiDiaryDTO 객체 생성 + Airecll_Scs_DTO airecall_scs_dto = new Airecll_Scs_DTO( + "scs", + LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 7), + "시점 회고 내용의 핵심 요약", + "시작 시점에서의 강점 또는 긍정적 측면 분석", + "시작 시점 개선할 점 또는 제안.", + "진행 중 회고 내용의 핵심 요약.", + "진행 중 강점 또는 긍정적 측면 분석.", + "진행 중 개선할 점 또는 제안.", + "종료 시점 회고 내용 요약.", + "종료 시점 강점 또는 긍정적 측면 분석.", + "종료 시점 개선할 점 또는 제안.", + "시작 시점 개선 계획.", + "진행 중 개선 계획.", + "종료 시점 개선 계획.", + user.getUsername() + ); + + // AiDiaryDTO를 AiDiary 엔티티로 변환 + airecall_scs = Airecall_scs.toairecallscsEntity(airecall_scs_dto, user); + + airecall_scs = aiRecallscsRpository.save(airecall_scs); + } + + @DisplayName("AI scs 분석 결과 보기") + @Test + void scs리포트분석() { + Airecall_scs found = aiRecallscsRpository.findById(airecall_scs.getAir_id()) + .orElseThrow(() -> new RuntimeException("데이터를 찾을 수 없습니다.")); + + assertNotNull(found, "저장된 데이터가 null입니다."); + assertEquals("scs", found.getType(), "SCS 타입이 다릅니다."); + assertEquals(LocalDate.of(2024, 2, 1), found.getStart_date(), "시작일자가 다릅니다."); + assertEquals(LocalDate.of(2024, 2, 7), found.getEnd_date(), "종료일자가 다릅니다."); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/ai_recall/service/AiRecallServiceTest.java b/src/test/java/com/woozuda/backend/ai_recall/service/AiRecallServiceTest.java deleted file mode 100644 index 3fc8bdc2..00000000 --- a/src/test/java/com/woozuda/backend/ai_recall/service/AiRecallServiceTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.woozuda.backend.ai_recall.service; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@SpringBootTest -@Transactional -class AiRecallServiceTest { - private static final Logger logger = LoggerFactory.getLogger(AiRecallServiceTest.class); - - -} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/diary/repository/DiaryRepositoryTest.java b/src/test/java/com/woozuda/backend/diary/repository/DiaryRepositoryTest.java index 1a0275a5..2e94f9e6 100644 --- a/src/test/java/com/woozuda/backend/diary/repository/DiaryRepositoryTest.java +++ b/src/test/java/com/woozuda/backend/diary/repository/DiaryRepositoryTest.java @@ -3,12 +3,14 @@ import com.woozuda.backend.account.entity.UserEntity; import com.woozuda.backend.diary.dto.response.SingleDiaryResponseDto; import com.woozuda.backend.diary.entity.Diary; +import com.woozuda.backend.note.entity.converter.AesEncryptor; import com.woozuda.backend.tag.entity.Tag; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -18,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@MockBean(AesEncryptor.class) @Transactional class DiaryRepositoryTest { diff --git a/src/test/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImplTest.java b/src/test/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImplTest.java deleted file mode 100644 index cd5eb492..00000000 --- a/src/test/java/com/woozuda/backend/forai/repository/CustomNoteRepoForAiImplTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.woozuda.backend.forai.repository; - -import com.woozuda.backend.diary.entity.Diary; -import com.woozuda.backend.forai.dto.CountRecallDto; -import com.woozuda.backend.note.entity.CommonNote; -import com.woozuda.backend.note.entity.QuestionNote; -import com.woozuda.backend.note.entity.type.Framework; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -@Transactional -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 실제 DB 사용 -class CustomNoteRepoForAiImplTest { - - @Autowired - private CustomNoteRepoForAi customNoteRepoForAi; // 테스트할 대상 - - @Autowired - private EntityManager em; - - @Test - void testCountAiDiary() { - // Given: 테스트 데이터 삽입 - String username = "woozuda@gmail.com"; - LocalDate startDate = LocalDate.of(2024, 12, 15); - LocalDate endDate = LocalDate.of(2024, 12, 22); - // When: CountAiDiary 메서드 호출 - long result = customNoteRepoForAi.aiDiaryCount(username, startDate, endDate); - // Then: 결과 검증 - System.out.println("result " + result); - - } -} \ No newline at end of file diff --git a/src/test/java/com/woozuda/backend/image/ImageDeleteCronTest.java b/src/test/java/com/woozuda/backend/image/ImageDeleteCronTest.java new file mode 100644 index 00000000..296c4815 --- /dev/null +++ b/src/test/java/com/woozuda/backend/image/ImageDeleteCronTest.java @@ -0,0 +1,64 @@ +package com.woozuda.backend.image; + +import com.woozuda.backend.image.config.S3Client; +import com.woozuda.backend.image.cron.ImageDeleteTask; +import com.woozuda.backend.image.entity.Image; +import com.woozuda.backend.image.repository.ImageRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.note.entity.converter.AesEncryptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import(ImageDeleteTask.class) +public class ImageDeleteCronTest { + + @MockBean + private AesEncryptor aesEncryptor; + + @MockBean + private ImageService imageService; + + @Autowired + private ImageDeleteTask imageDeleteTask; + + @Autowired + private ImageRepository imageRepository; + + @BeforeEach + void setUp() { + imageRepository.deleteAll(); + + List images = new ArrayList<>(); + images.add(Image.of("https://kr.object.ncloudstorage.com/test/aaa.com", false)); + images.add(Image.of("https://kr.object.ncloudstorage.com/test/bbb.com", true)); + images.add(Image.of("https://kr.object.ncloudstorage.com/test/ccc.com", false)); + images.add(Image.of("https://kr.object.ncloudstorage.com/test/ddd.com", true)); + images.add(Image.of("https://kr.object.ncloudstorage.com/test/eee.com", false)); + + imageRepository.saveAll(images); + } + + @Test + void 크론잡_테스트() { + //when + imageDeleteTask.cleanUpImages(); + + //then + assertThat(imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/test/aaa.com")).isEqualTo(null); + assertThat(imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/test/ccc.com")).isEqualTo(null); + assertThat(imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/test/eee.com")).isEqualTo(null); + + assertThat(imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/test/bbb.com").getIsLinkedToPost()).isEqualTo(true); + assertThat(imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/test/ddd.com").getIsLinkedToPost()).isEqualTo(true); + } +} diff --git a/src/test/java/com/woozuda/backend/image/ImageServiceTest.java b/src/test/java/com/woozuda/backend/image/ImageServiceTest.java new file mode 100644 index 00000000..08cfbe7c --- /dev/null +++ b/src/test/java/com/woozuda/backend/image/ImageServiceTest.java @@ -0,0 +1,407 @@ +package com.woozuda.backend.image; + +import com.woozuda.backend.image.config.S3Client; +import com.woozuda.backend.image.entity.Image; +import com.woozuda.backend.image.repository.ImageRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.image.type.ImageType; +import com.woozuda.backend.note.entity.converter.AesEncryptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import(ImageService.class) +public class ImageServiceTest { + + @MockBean + private AesEncryptor aesEncryptor; + + @MockBean + private S3Client s3Client; + + @Autowired + private ImageService imageService; + + @Autowired + private ImageRepository imageRepository; + + @BeforeEach + void setUp(){ + imageRepository.deleteAll(); + + List images = new ArrayList<>(); + images.add(Image.of("https://aaa.com", false)); + images.add(Image.of("https://bbb.com", false)); + images.add(Image.of("https://ccc.com", false)); + images.add(Image.of("https://ddd.com", false)); + images.add(Image.of("https://eee.com", false)); + + imageRepository.saveAll(images); + } + // 테스트 해야 할 것 + // 1. 다이어리 / 일기의 생성 + @Test + void 기본이미지_다이어리_생성() { + + // when (랜덤 이미지로 다이어리 생성) + imageService.afterCreate(ImageType.DIARY, 1L, "https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + + // then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 커스텀이미지_다이어리_생성(){ + // when (aaa.com 이라는 커스텀 이미지로 다이어리를 생성했다면) + imageService.afterCreate(ImageType.DIARY, 3L, "https://aaa.com"); + + // then + //aaa.com 은 true 가 되어야 함 + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getConnectedId()).isEqualTo(3L); + assertThat(tempImage1.getImageType()).isEqualTo(ImageType.DIARY); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(true); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 이미지_없는_일기_생성(){ + // when (aaa.com 이라는 커스텀 이미지로 다이어리를 생성했다면) + imageService.afterCreate(ImageType.NOTE, 3L, " 하하하

나는 p 에요

하하하

나는 제목이에요

"); + + // then + //aaa.com 은 true 가 되어야 함 + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 이미지_있는_일기_생성(){ + // when (aaa.com 이라는 커스텀 이미지로 다이어리를 생성했다면) + imageService.afterCreate(ImageType.NOTE, 3L, " 하하하

나는 p 에요

나는 제목이에요

"); + + // then + //aaa.com 은 true 가 되어야 함 + + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getConnectedId()).isEqualTo(3L); + assertThat(tempImage1.getImageType()).isEqualTo(ImageType.NOTE); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(true); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + //2. 다이어리 / 일기의 수정 + @Test + void 다이어리_기본이미지에서_기본이미지로_수정(){ + //when + imageService.afterUpdate(ImageType.DIARY, 1L, "https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 다이어리_기본이미지에서_커스텀이지미지로_수정(){ + //when + imageService.afterUpdate(ImageType.DIARY, 3L, "https://aaa.com"); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getConnectedId()).isEqualTo(3L); + assertThat(tempImage1.getImageType()).isEqualTo(ImageType.DIARY); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(true); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 다이어리_커스텀이미지에서_기본이미지로_수정(){ + //given ( 기존 다이어리는 id 가 3이고, aaa.com 을 표지로 한다고 가정) + + Image prevImage = imageRepository.findByImageUrl("https://aaa.com"); + prevImage.changeImageType(ImageType.DIARY); + prevImage.changeConectedId(3L); + prevImage.changeLinkedToPost(true); + + //when + imageService.afterUpdate(ImageType.DIARY, 3L, "https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1).isEqualTo(null); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 일기_수정(){ + //given ( 기존 일기장 id 가 3이고, aaa.com , ccc.com 을 가지고 있던 일기 라고 가정) + Image prevImage = imageRepository.findByImageUrl("https://aaa.com"); + prevImage.changeImageType(ImageType.NOTE); + prevImage.changeConectedId(3L); + prevImage.changeLinkedToPost(true); + + Image prevImage2 = imageRepository.findByImageUrl("https://ccc.com"); + prevImage.changeImageType(ImageType.NOTE); + prevImage.changeConectedId(3L); + prevImage.changeLinkedToPost(true); + + //when ( aaa.com 지우고 bbb.com, ddd.com 추가, ccc.com 은 그대로 ) + imageService.afterUpdate(ImageType.NOTE, 3L, " 하하하

나는 p 에요

나는 제목이에요

" + +" "); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1).isEqualTo(null); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getConnectedId()).isEqualTo(3L); + assertThat(tempImage2.getImageType()).isEqualTo(ImageType.NOTE); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(true); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getConnectedId()).isEqualTo(3L); + assertThat(tempImage3.getImageType()).isEqualTo(ImageType.NOTE); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(true); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getConnectedId()).isEqualTo(3L); + assertThat(tempImage4.getImageType()).isEqualTo(ImageType.NOTE); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(true); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + + //3. 다이어리 / 일기의 삭제 + @Test + void 커스텀이미지_다이어리_삭제(){ + //given (다이어리 id 가 3이고 aaa.com 을 표지로 삼고 있음) + Image prevImage = imageRepository.findByImageUrl("https://aaa.com"); + prevImage.changeImageType(ImageType.DIARY); + prevImage.changeConectedId(3L); + prevImage.changeLinkedToPost(true); + + //when ( aaa.com 지우고 bbb.com, ddd.com 추가, ccc.com 은 그대로 ) + imageService.afterDelete(ImageType.DIARY, 3L); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1).isEqualTo(null); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 기본이미지_다이어리_삭제(){ + //given + + //when ( aaa.com 지우고 bbb.com, ddd.com 추가, ccc.com 은 그대로 ) + imageService.afterDelete(ImageType.NOTE, 3L); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 이미지있는_일기_삭제(){ + //given (다이어리 id 가 3이고 aaa.com 을 표지로 삼고 있음) + Image prevImage = imageRepository.findByImageUrl("https://aaa.com"); + prevImage.changeImageType(ImageType.NOTE); + prevImage.changeConectedId(3L); + prevImage.changeLinkedToPost(true); + + //when ( aaa.com 지우고 bbb.com, ddd.com 추가, ccc.com 은 그대로 ) + imageService.afterDelete(ImageType.NOTE, 3L); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1).isEqualTo(null); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + + @Test + void 이미지없는_일기_삭제(){ + //given (다이어리 id 가 3이고 aaa.com 을 표지로 삼고 있음) + + //when ( aaa.com 지우고 bbb.com, ddd.com 추가, ccc.com 은 그대로 ) + imageService.afterDelete(ImageType.DIARY, 3L); + + //then + Image tempImage1 = imageRepository.findByImageUrl("https://aaa.com"); + assertThat(tempImage1.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage2 = imageRepository.findByImageUrl("https://bbb.com"); + assertThat(tempImage2.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage3 = imageRepository.findByImageUrl("https://ccc.com"); + assertThat(tempImage3.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage4 = imageRepository.findByImageUrl("https://ddd.com"); + assertThat(tempImage4.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage5 = imageRepository.findByImageUrl("https://eee.com"); + assertThat(tempImage5.getIsLinkedToPost()).isEqualTo(false); + + Image tempImage6 = imageRepository.findByImageUrl("https://kr.object.ncloudstorage.com/woozuda-image/random-image/random-image-3.jpg"); + assertThat(tempImage6).isEqualTo(null); + } + +} diff --git a/src/test/java/com/woozuda/backend/image/MakeImgsUrlTest.java b/src/test/java/com/woozuda/backend/image/MakeImgsUrlTest.java new file mode 100644 index 00000000..4f1e1f9e --- /dev/null +++ b/src/test/java/com/woozuda/backend/image/MakeImgsUrlTest.java @@ -0,0 +1,51 @@ +package com.woozuda.backend.image; + +import com.woozuda.backend.image.config.S3Client; +import com.woozuda.backend.image.repository.ImageRepository; +import com.woozuda.backend.image.service.ImageService; +import com.woozuda.backend.image.type.ImageType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +public class MakeImgsUrlTest { + + @InjectMocks + ImageService imageService; + + @Mock + S3Client s3Client; + + @Mock + ImageRepository imageRepository; + + @Test + public void imageUrl_파서_테스트_노트() { + List urls= imageService.makeImgsUrl(ImageType.NOTE, "

나는

운동을 했다

" + + "

나는

잠을 잤다

"); + + assertThat(urls).contains("https://aaa.com", "https://bbb.com"); + } + + @Test + public void imageUrl_파서_테스트_노트_없을때() { + List urls= imageService.makeImgsUrl(ImageType.NOTE, "

나는

운동을 했다

" + + "

나는

잠을 잤다

"); + + assertThat(urls).isEmpty(); + } + + @Test + public void imageUrl_파서_테스트_다이어리() { + List urls= imageService.makeImgsUrl(ImageType.DIARY, "https://aaa.com"); + + assertThat(urls).contains("https://aaa.com"); + } +} diff --git a/src/test/java/com/woozuda/backend/note/repository/NoteRepositoryTest.java b/src/test/java/com/woozuda/backend/note/repository/NoteRepositoryTest.java index 64535dd5..172a0947 100644 --- a/src/test/java/com/woozuda/backend/note/repository/NoteRepositoryTest.java +++ b/src/test/java/com/woozuda/backend/note/repository/NoteRepositoryTest.java @@ -4,26 +4,48 @@ import com.woozuda.backend.diary.entity.Diary; import com.woozuda.backend.note.dto.request.NoteCondRequestDto; import com.woozuda.backend.note.dto.response.NoteResponseDto; -import com.woozuda.backend.note.entity.*; +import com.woozuda.backend.note.entity.CommonNote; +import com.woozuda.backend.note.entity.NoteContent; +import com.woozuda.backend.note.entity.QuestionNote; +import com.woozuda.backend.note.entity.RetrospectiveNote; +import com.woozuda.backend.note.entity.converter.AesEncryptor; +import com.woozuda.backend.note.entity.converter.NoteContentConverter; +import com.woozuda.backend.question.entity.Question; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.List; import static com.woozuda.backend.account.entity.AiType.PICTURE_NOVEL; -import static com.woozuda.backend.note.entity.type.Feeling.*; -import static com.woozuda.backend.note.entity.type.Framework.*; +import static com.woozuda.backend.note.entity.type.Feeling.ANGER; +import static com.woozuda.backend.note.entity.type.Feeling.CONTENT; +import static com.woozuda.backend.note.entity.type.Feeling.DISSATISFACTION; +import static com.woozuda.backend.note.entity.type.Feeling.JOY; +import static com.woozuda.backend.note.entity.type.Feeling.NEUTRAL; +import static com.woozuda.backend.note.entity.type.Feeling.SADNESS; +import static com.woozuda.backend.note.entity.type.Feeling.TIREDNESS; +import static com.woozuda.backend.note.entity.type.Framework.FOUR_F_S; +import static com.woozuda.backend.note.entity.type.Framework.KPT; +import static com.woozuda.backend.note.entity.type.Framework.PMI; +import static com.woozuda.backend.note.entity.type.Framework.SCS; import static com.woozuda.backend.note.entity.type.Season.FALL; import static com.woozuda.backend.note.entity.type.Season.WINTER; import static com.woozuda.backend.note.entity.type.Visibility.PRIVATE; import static com.woozuda.backend.note.entity.type.Visibility.PUBLIC; -import static com.woozuda.backend.note.entity.type.Weather.*; +import static com.woozuda.backend.note.entity.type.Weather.CLEAR; +import static com.woozuda.backend.note.entity.type.Weather.CLOUDY; +import static com.woozuda.backend.note.entity.type.Weather.RAIN; +import static com.woozuda.backend.note.entity.type.Weather.SNOW; +import static com.woozuda.backend.note.entity.type.Weather.SUNNY; +import static com.woozuda.backend.note.entity.type.Weather.THUNDERSTORM; import static java.time.Month.DECEMBER; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +53,19 @@ @Transactional class NoteRepositoryTest { + @TestConfiguration + static class NoteContentTestConfig { + @Bean + public AesEncryptor aesEncryptor() { + return new AesEncryptor("test-password"); + } + + @Bean + public NoteContentConverter noteContentConverter(AesEncryptor aesEncryptor) { + return new NoteContentConverter(aesEncryptor); + } + } + LocalDate date1 = LocalDate.of(2024, DECEMBER, 5); LocalDate date2 = LocalDate.of(2024, DECEMBER, 6); diff --git a/src/test/java/com/woozuda/backend/note/repository/QuestionRepositoryTest.java b/src/test/java/com/woozuda/backend/note/repository/QuestionRepositoryTest.java index ea8afeaf..5cb78be9 100644 --- a/src/test/java/com/woozuda/backend/note/repository/QuestionRepositoryTest.java +++ b/src/test/java/com/woozuda/backend/note/repository/QuestionRepositoryTest.java @@ -1,21 +1,22 @@ package com.woozuda.backend.note.repository; -import com.woozuda.backend.note.entity.Question; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.Disabled; +import com.woozuda.backend.note.entity.converter.AesEncryptor; +import com.woozuda.backend.question.entity.Question; +import com.woozuda.backend.question.repository.QuestionRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; -import java.lang.reflect.Field; import java.time.LocalDate; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@MockBean(AesEncryptor.class) @Transactional class QuestionRepositoryTest { diff --git a/src/test/java/com/woozuda/backend/security/filter/IPCheckFilterUnitTest.java b/src/test/java/com/woozuda/backend/security/filter/IPCheckFilterUnitTest.java new file mode 100644 index 00000000..0b46b9c9 --- /dev/null +++ b/src/test/java/com/woozuda/backend/security/filter/IPCheckFilterUnitTest.java @@ -0,0 +1,67 @@ +package com.woozuda.backend.security.filter; + +import com.woozuda.backend.security.jwt.IPCheckFilter; +import com.woozuda.backend.security.jwt.JWTUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; + +//IPCheckFilter 를 단위 테스트 +@ExtendWith(MockitoExtension.class) +public class IPCheckFilterUnitTest { + + private IPCheckFilter ipCheckFilter; + + @Mock + private JWTUtil jwtUtil; + + @BeforeEach + void setUp() { + List adminIps = List.of("127.0.0.1"); + ipCheckFilter = new IPCheckFilter(adminIps); + } + + @Test + void IPFilter_성공_허가ip() throws Exception{ + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + when(request.getHeader("X-Forwarded-For")).thenReturn("127.0.0.1"); + + ipCheckFilter.doFilter(request, response, filterChain); + + verify(response, times(0)).sendError(HttpServletResponse.SC_FORBIDDEN, "IP not allowed"); + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + void IPFilter_실패_비허가ip() throws Exception{ + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + when(request.getHeader("X-Forwarded-For")).thenReturn("1.2.3.4"); + + ipCheckFilter.doFilter(request, response, filterChain); + + verify(response, times(1)).sendError(HttpServletResponse.SC_FORBIDDEN, "IP not allowed"); + verify(filterChain, times(0)).doFilter(request, response); + } + + + +} diff --git a/src/test/java/com/woozuda/backend/security/filter/JWTFilterUnitTest.java b/src/test/java/com/woozuda/backend/security/filter/JWTFilterUnitTest.java new file mode 100644 index 00000000..e9cfde15 --- /dev/null +++ b/src/test/java/com/woozuda/backend/security/filter/JWTFilterUnitTest.java @@ -0,0 +1,194 @@ +package com.woozuda.backend.security.filter; + +import com.woozuda.backend.security.jwt.JWTFilter; +import com.woozuda.backend.security.jwt.JWTUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +//JWTFilter를 단위 테스트 하기 위한 클래스 +@ExtendWith(MockitoExtension.class) +public class JWTFilterUnitTest { + + @InjectMocks + private JWTFilter jwtFilter; + + @Mock + private JWTUtil jwtUtil; + + @BeforeEach + void setUp(){ + SecurityContextHolder.clearContext(); + } + + @Test + void JWTFilter_성공_일반유저() throws Exception{ + //given + + //JWTFilter는 HttpServletRequest request, HttpServletResponse response, FilterChain filterChain 를 인수로 받음 (*OncePerRequestFilter를 상속할 시 해당됨) + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + Cookie[] cookies ={ + new Cookie("Authorization", "goodtoken") + }; + + when(request.getCookies()).thenReturn(cookies); + when(jwtUtil.isExpired("goodtoken")).thenReturn(false); + when(jwtUtil.getUsername("goodtoken")).thenReturn("gooduser"); + when(jwtUtil.getRole("goodtoken")).thenReturn("ROLE_USER"); + + + //when + // doFilter 가 doFilterInternal 을 호출 + jwtFilter.doFilter(request, response, filterChain); + + //then + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNotNull(); + assertThat(authentication.isAuthenticated()).isEqualTo(true); + assertThat(authentication.getName()).isEqualTo("gooduser"); + + List authorityNames = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + assertThat(authorityNames).contains("ROLE_USER"); + + + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + void JWTFilter_성공_어드민() throws Exception{ + //given + + //JWTFilter는 HttpServletRequest request, HttpServletResponse response, FilterChain filterChain 를 인수로 받음 (*OncePerRequestFilter를 상속할 시 해당됨) + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + Cookie[] cookies ={ + new Cookie("Authorization", "goodtoken") + }; + + when(request.getCookies()).thenReturn(cookies); + when(jwtUtil.isExpired("goodtoken")).thenReturn(false); + when(jwtUtil.getUsername("goodtoken")).thenReturn("gooduser"); + when(jwtUtil.getRole("goodtoken")).thenReturn("ROLE_ADMIN"); + + + //when + // doFilter 가 doFilterInternal 을 호출 + jwtFilter.doFilter(request, response, filterChain); + + //then + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNotNull(); + assertThat(authentication.isAuthenticated()).isEqualTo(true); + assertThat(authentication.getName()).isEqualTo("gooduser"); + + List authorityNames = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + assertThat(authorityNames).contains("ROLE_ADMIN"); + + + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + void JWTFilter_실패_쿠키null() throws Exception{ + //given + + //JWTFilter는 HttpServletRequest request, HttpServletResponse response, FilterChain filterChain 를 인수로 받음 (*OncePerRequestFilter를 상속할 시 해당됨) + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + when(request.getCookies()).thenReturn(null); + + + //when + // doFilter 가 doFilterInternal 을 호출 + jwtFilter.doFilter(request, response, filterChain); + + //then + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNull(); + + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + void JWTFilter_실패_Authorization이없는쿠키() throws Exception{ + //given + + //JWTFilter는 HttpServletRequest request, HttpServletResponse response, FilterChain filterChain 를 인수로 받음 (*OncePerRequestFilter를 상속할 시 해당됨) + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + Cookie[] cookies ={ + new Cookie("A", "goodtoken") + }; + + when(request.getCookies()).thenReturn(cookies); + + //when + // doFilter 가 doFilterInternal 을 호출 + jwtFilter.doFilter(request, response, filterChain); + + //then + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNull(); + + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + void JWTFilter_실패_만료된토큰() throws Exception{ + //given + + //JWTFilter는 HttpServletRequest request, HttpServletResponse response, FilterChain filterChain 를 인수로 받음 (*OncePerRequestFilter를 상속할 시 해당됨) + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + Cookie[] cookies ={ + new Cookie("Authorization", "expiredtoken") + }; + + when(request.getCookies()).thenReturn(cookies); + when(jwtUtil.isExpired("expiredtoken")).thenReturn(true); + + + //when + // doFilter 가 doFilterInternal 을 호출 + jwtFilter.doFilter(request, response, filterChain); + + //then + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNull(); + + verify(filterChain, times(1)).doFilter(request, response); + } + +} diff --git a/src/test/java/com/woozuda/backend/security/login/LoginTest.java b/src/test/java/com/woozuda/backend/security/login/LoginTest.java new file mode 100644 index 00000000..c5aeb759 --- /dev/null +++ b/src/test/java/com/woozuda/backend/security/login/LoginTest.java @@ -0,0 +1,171 @@ +package com.woozuda.backend.security.login; + +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.security.jwt.JWTUtil; +import com.woozuda.backend.shortlink.repository.ShortLinkRepository; +import com.woozuda.backend.testutil.UserEntityBuilder; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +// 로그인(/login) 에 대한 통합 테스트 +@SpringBootTest +@AutoConfigureMockMvc +public class LoginTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ShortLinkRepository shortLinkRepository; + + @Autowired + private JWTUtil jwtUtil; + + @BeforeEach + void setUp(){ + UserEntityBuilder.resetCounter(); + shortLinkRepository.deleteAll(); + userRepository.deleteAll(); + userRepository.save(UserEntityBuilder.createUniqueUser().build()); + } + + + @Test + public void 로그인_성공() throws Exception { + + //given + String requestBody = "{\"username\":\"user1@gmail.com\", \"password\":\"1234\"}"; + + //when + MvcResult mvcResult = mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isOk()) + .andReturn(); + + MockHttpServletResponse response = mvcResult.getResponse(); + Cookie[] cookies = response.getCookies(); + + String authorization = ""; + for (Cookie cookie : cookies) { + + //System.out.println(cookie.getName()); + if (cookie.getName().equals("Authorization")) { + + authorization = cookie.getValue(); + } + } + + //then ( 정상적인 토큰이 발급 되었는지 확인 ) + assertThat(jwtUtil.getUsername(authorization)).isEqualTo("user1@gmail.com"); + assertThat(jwtUtil.isExpired(authorization)).isEqualTo(false); + + } + + @Test + public void 로그인_실패_없는계정() throws Exception { + + //given + String requestBody = "{\"username\":\"no@gmail.com\", \"password\":\"1234\"}"; + + //when + MvcResult mvcResult = mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isUnauthorized()) + .andReturn(); + + MockHttpServletResponse response = mvcResult.getResponse(); + Cookie[] cookies = response.getCookies(); + + String authorization = ""; + for (Cookie cookie : cookies) { + + //System.out.println(cookie.getName()); + if (cookie.getName().equals("Authorization")) { + + authorization = cookie.getValue(); + } + } + + //then ( 정상적인 토큰이 발급 되었는지 확인 ) + assertThat(authorization).isEqualTo(""); + } + + @Test + public void 로그인_실패_비밀번호틀림() throws Exception { + + //given + String requestBody = "{\"username\":\"user1@gmail.com\", \"password\":\"5555\"}"; + + //when + MvcResult mvcResult = mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isUnauthorized()) + .andReturn(); + + MockHttpServletResponse response = mvcResult.getResponse(); + Cookie[] cookies = response.getCookies(); + + String authorization = ""; + for (Cookie cookie : cookies) { + + //System.out.println(cookie.getName()); + if (cookie.getName().equals("Authorization")) { + + authorization = cookie.getValue(); + } + } + + //then ( 정상적인 토큰이 발급 되었는지 확인 ) + assertThat(authorization).isEqualTo(""); + } + + + @Test + public void 로그인_실패_empty_json() throws Exception { + + //given + String requestBody = "{}"; + + //when + MvcResult mvcResult = mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isUnauthorized()) + .andReturn(); + + MockHttpServletResponse response = mvcResult.getResponse(); + Cookie[] cookies = response.getCookies(); + + String authorization = ""; + for (Cookie cookie : cookies) { + + //System.out.println(cookie.getName()); + if (cookie.getName().equals("Authorization")) { + + authorization = cookie.getValue(); + } + } + + //then ( 정상적인 토큰이 발급 되었는지 확인 ) + assertThat(authorization).isEqualTo(""); + } +} diff --git a/src/test/java/com/woozuda/backend/security/oauth2/OAuth2ResponseFactoryUnitTest.java b/src/test/java/com/woozuda/backend/security/oauth2/OAuth2ResponseFactoryUnitTest.java new file mode 100644 index 00000000..c4c29d07 --- /dev/null +++ b/src/test/java/com/woozuda/backend/security/oauth2/OAuth2ResponseFactoryUnitTest.java @@ -0,0 +1,92 @@ +package com.woozuda.backend.security.oauth2; + +import com.woozuda.backend.account.transdata.OAuth2Response; +import com.woozuda.backend.account.transdata.OAuth2ResponseFactory; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OAuth2ResponseFactoryUnitTest { + + // oauth2 소셜 로그인 단위 테스트 + // 테스트 구간 - CustomUserDetailsService 의 일부 : oAuth2Response = OAuth2ResponseFactory.getOAuth2Response(provider, oAuth2User.getAttributes()); + + @Test + public void 네이버_로그인() throws Exception { + // naver: {resultcode=00, message=success, response={id=oz7UDpVICFBzNmLB-mwZHzgR7IIY7-Y02Az8vYPMwyY, email=enjoying1018@naver.com, name=이동현}} + Map attribute = new HashMap<>(); + attribute.put("resultcode", "00"); + attribute.put("message", "success"); + + + Map response = new HashMap<>(); + response.put("id", "oz7UDpVICFBzNmLB-mwZHzgR7IIY7-Y02Az8vYPMwyY"); + response.put("email", "enjoying1018@naver.com"); + response.put("name", "이동현"); + attribute.put("response", response); + + OAuth2Response oauth2Response = OAuth2ResponseFactory.getOAuth2Response("naver", attribute); + + assertThat(oauth2Response.getProvider()).isEqualTo("naver"); + assertThat(oauth2Response.getEmail()).isEqualTo("enjoying1018@naver.com"); + assertThat(oauth2Response.getProviderId()).isEqualTo("oz7UDpVICFBzNmLB-mwZHzgR7IIY7-Y02Az8vYPMwyY"); + } + + @Test + public void 구글_로그인() throws Exception { + //google : {sub=114239676830803412592, name=Dong Hyeon Lee, given_name=Dong Hyeon, family_name=Lee, picture=https://lh3.googleusercontent.com/a/ACg8ocIkBClm_OfS47EdZK209e7dnd-ZcSiJjLvJHQjhoTx5CGcGrw=s96-c, email=ske04186@gmail.com, email_verified=true} + Map attribute = new HashMap<>(); + attribute.put("sub", "114239676830803412592"); + attribute.put("name", "Dong Hyeon Lee"); + attribute.put("family_name", "Lee"); + attribute.put("picture", "https://lh3.googleusercontent.com/a/ACg8ocIkBClm_OfS47EdZK209e7dnd-ZcSiJjLvJHQjhoTx5CGcGrw=s96-c"); + attribute.put("email", "ske04186@gmail.com"); + attribute.put("email_verified", true); + + OAuth2Response oauth2Response = OAuth2ResponseFactory.getOAuth2Response("google", attribute); + + assertThat(oauth2Response.getProvider()).isEqualTo("google"); + assertThat(oauth2Response.getEmail()).isEqualTo("ske04186@gmail.com"); + assertThat(oauth2Response.getProviderId()).isEqualTo("114239676830803412592"); + + } + + @Test + public void 카카오_로그인() throws Exception { + // kakao : {id=3821328965, connected_at=2024-12-05T07:19:11Z, properties={nickname=이동현}, kakao_account={profile_nickname_needs_agreement=false, profile={nickname=이동현, is_default_nickname=false}, + // has_email=true, email_needs_agreement=false, is_email_valid=true, is_email_verified=true, email=enjoying1018@daum.net}} + Map attribute = new HashMap<>(); + attribute.put("id", "3821328965"); + attribute.put("connected_at", "2024-12-05T07:19:11Z"); + + + Map properties = new HashMap<>(); + properties.put("nickname", "이동현"); + attribute.put("properties", properties); + + Map kakaoAccount = new HashMap<>(); + kakaoAccount.put("profile_nickname_needs_agrrement", false); + + Map profile = new HashMap<>(); + profile.put("nickname", "이동현"); + profile.put("is_default_nickname", false); + kakaoAccount.put("profile", profile); + + kakaoAccount.put("has_email", true); + kakaoAccount.put("email_needs_agreement", false); + kakaoAccount.put("is_email_valid", true); + kakaoAccount.put("is_email_verified", true); + kakaoAccount.put("email", "enjoying1018@daum.net"); + + attribute.put("kakao_account", kakaoAccount); + + OAuth2Response oauth2Response = OAuth2ResponseFactory.getOAuth2Response("kakao", attribute); + + assertThat(oauth2Response.getProvider()).isEqualTo("kakao"); + assertThat(oauth2Response.getEmail()).isEqualTo("enjoying1018@daum.net"); + assertThat(oauth2Response.getProviderId()).isEqualTo("3821328965"); + } +} diff --git a/src/test/java/com/woozuda/backend/shortlink/service/ShareServiceTest.java b/src/test/java/com/woozuda/backend/shortlink/service/ShareServiceTest.java index bf4c8fff..06106c06 100644 --- a/src/test/java/com/woozuda/backend/shortlink/service/ShareServiceTest.java +++ b/src/test/java/com/woozuda/backend/shortlink/service/ShareServiceTest.java @@ -12,6 +12,7 @@ import com.woozuda.backend.shortlink.dto.note.SharedNoteResponseDto; import com.woozuda.backend.shortlink.entity.ShortLink; import com.woozuda.backend.shortlink.repository.ShortLinkRepository; +import com.woozuda.backend.testutil.UserEntityBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ import java.time.LocalDate; import java.util.Arrays; +import java.util.List; import static com.woozuda.backend.account.entity.AiType.PICTURE_NOVEL; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -73,7 +75,16 @@ void setUp(){ void makeSharedNoteTest() throws Exception { //given - 데이터 넣기 (user 1명, diary 1개, question 1개 ,note 5개) - UserEntity user = new UserEntity(null, "woozuda@gmail.com", "1234", "ROLE_ADMIN", PICTURE_NOVEL, true, "woozuda@gmail.com", "woozuda"); + + // before + // UserEntity user = new UserEntity(null, "woozuda@gmail.com", "1234", "ROLE_ADMIN", PICTURE_NOVEL, true, "woozuda@gmail.com", "woozuda"); + + // after + UserEntity user = UserEntityBuilder.createUniqueUser() + .withPassword("1234") + .withEmail("woozuda@gmail.com") + .build(); + userRepository.save(user); Diary diary1 = Diary.of(user, "https://woozuda-image.kr.object.ncloudstorage.com/random-image-1.jpg", "my first diary"); @@ -122,13 +133,19 @@ void makeSharedNoteTest() throws Exception { void getSharedNoteTest() throws Exception { //given - 데이터 넣기 (user 1명, diary 1개, question 1개 ,note 5개) - UserEntity user1 = new UserEntity(null, "woozuda@gmail.com", "1234", "ROLE_ADMIN", PICTURE_NOVEL, true, "woozuda@gmail.com", "woozuda"); - UserEntity user2 = new UserEntity(null, "rodom1018@gmail.com", "1234", "ROLE_ADMIN", PICTURE_NOVEL, true, "rodom1018@gmail.com", "woozuda"); - userRepository.save(user1); - userRepository.save(user2); - Diary diary1 = Diary.of(user1, "https://woozuda-image.kr.object.ncloudstorage.com/random-image-1.jpg", "my first diary"); - Diary diary2 = Diary.of(user2, "https://woozuda-image.kr.object.ncloudstorage.com/random-image-1.jpg", "my first diary22"); + //Before + //UserEntity user1 = new UserEntity(null, "woozuda@gmail.com", "1234", "ROLE_ADMIN", PICTURE_NOVEL, true, "woozuda@gmail.com", "woozuda"); + //UserEntity user2 = new UserEntity(null, "rodom1018@gmail.com", "1234", "ROLE_ADMIN", PICTURE_NOVEL, true, "rodom1018@gmail.com", "woozuda"); + //userRepository.save(user1); + //userRepository.save(user2); + + //after + List userEntityList = UserEntityBuilder.createUniqueMultipleUser(10); + userRepository.saveAll(userEntityList); + + Diary diary1 = Diary.of(userEntityList.get(0), "https://woozuda-image.kr.object.ncloudstorage.com/random-image-1.jpg", "my first diary"); + Diary diary2 = Diary.of(userEntityList.get(1), "https://woozuda-image.kr.object.ncloudstorage.com/random-image-1.jpg", "my first diary22"); diaryRepository.save(diary1); diaryRepository.save(diary2); diff --git a/src/test/java/com/woozuda/backend/shortlink/service/SharedShortLinkTest.java b/src/test/java/com/woozuda/backend/shortlink/service/SharedShortLinkTest.java new file mode 100644 index 00000000..5378f43c --- /dev/null +++ b/src/test/java/com/woozuda/backend/shortlink/service/SharedShortLinkTest.java @@ -0,0 +1,248 @@ +package com.woozuda.backend.shortlink.service; + +import com.woozuda.backend.account.entity.UserEntity; +import com.woozuda.backend.account.repository.UserRepository; +import com.woozuda.backend.ai_creation.entity.AiCreation; +import com.woozuda.backend.ai_creation.entity.CreationType; +import com.woozuda.backend.ai_creation.entity.CreationVisibility; +import com.woozuda.backend.ai_creation.repository.AiCreationRepository; +import com.woozuda.backend.diary.entity.Diary; +import com.woozuda.backend.diary.repository.DiaryRepository; +import com.woozuda.backend.note.entity.*; +import com.woozuda.backend.note.entity.type.*; +import com.woozuda.backend.note.repository.NoteRepository; +import com.woozuda.backend.question.entity.Question; +import com.woozuda.backend.question.repository.QuestionRepository; +import com.woozuda.backend.security.jwt.JWTUtil; +import com.woozuda.backend.shortlink.entity.ShortLink; +import com.woozuda.backend.shortlink.repository.ShortLinkRepository; +import com.woozuda.backend.testutil.UserEntityBuilder; +import jakarta.persistence.EntityManager; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class SharedShortLinkTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + UserRepository userRepository; + + @Autowired + ShortLinkRepository shortLinkRepository; + + @Autowired + DiaryRepository diaryRepository; + + @Autowired + QuestionRepository questionRepository; + + @Autowired + NoteRepository noteRepository; + + @Autowired + AiCreationRepository aiCreationRepository; + + @Autowired + JWTUtil jwtUtil; + + @BeforeEach + void setUp(){ + //초기화 + noteRepository.deleteAll(); + questionRepository.deleteAll(); + diaryRepository.deleteAll(); + shortLinkRepository.deleteAll(); + aiCreationRepository.deleteAll(); + userRepository.deleteAll(); + UserEntityBuilder.resetCounter(); + // 상황 설정 + // user1@gmail.com 는 공유 일기를 4개 가지고 있음 : 2025-1-1 에 2개 , 2025-1-10 에 2개 + // user1@gmail.com 는 공유 ai 컨텐츠를 가지고 있지 않음 + // user2@gmail.com 는 공유 일기를 가지고 있지 않음 + // user2@gmail.com 는 공유 ai 컨텐츠를 4개 가지고 있음 : 2025-1-1 에 2개, 2025-1-10 에 2개 + + // 1. 유저 저장 + List users = UserEntityBuilder.createUniqueMultipleUser(2); + userRepository.saveAll(users); + + // 2. 유저에 연결된 숏링크 저장 + ShortLink shortLink1 = new ShortLink(null, "user1hash", users.get(0)); + ShortLink shortLink2 = new ShortLink(null, "user2hash", users.get(1)); + shortLinkRepository.saveAll(Arrays.asList(shortLink1, shortLink2)); + + // 3. 다이어리 생성 + Diary diary1 = Diary.of(users.get(0), "https://sample/image", "user1의 다이어리"); + Diary diary2 = Diary.of(users.get(1), "https://sample/image2", "user2의 다이어리"); + diaryRepository.saveAll(Arrays.asList(diary1,diary2)); + + // 4-1. 질문 생성 + Question question1 = Question.of("질문1"); + questionRepository.save(question1); + + // 5-1. user1@gmail.com의 일기 생성 + Note note1 = CommonNote.of(diary1, "자유일기1", LocalDate.of(2025, 1, 1), Visibility.PUBLIC, Feeling.JOY, Weather.SUNNY, Season.WINTER); + Note note2 = CommonNote.of(diary1, "자유일기2", LocalDate.of(2025, 1, 1), Visibility.PUBLIC, Feeling.JOY, Weather.SUNNY, Season.WINTER); + Note note3 = CommonNote.of(diary1, "자유일기3", LocalDate.of(2025, 1, 1), Visibility.PRIVATE, Feeling.JOY, Weather.SUNNY, Season.WINTER); + Note note4 = QuestionNote.of(diary1, "질문일기1", LocalDate.of(2025, 1, 10), Visibility.PUBLIC, question1, Feeling.JOY, Weather.SUNNY, Season.WINTER); + Note note5 = RetrospectiveNote.of(diary1, "회고일기1", LocalDate.of(2025, 1, 10), Visibility.PUBLIC, Framework.KPT); + + List content1 = new ArrayList<>(Arrays.asList("자유일기1-내용")); + for (int i = 0; i < content1.size(); i++) { + NoteContent noteContent = NoteContent.of(i + 1, content1.get(i)); + note1.addContent(noteContent); + } + + List content2 = new ArrayList<>(Arrays.asList("Keep", "Problem", "Try")); + for (int i = 0; i < content2.size(); i++) { + NoteContent noteContent = NoteContent.of(i + 1, content2.get(i)); + note5.addContent(noteContent); + } + + noteRepository.saveAll(Arrays.asList(note1, note2, note3, note4, note5)); + + // 5-2. user2@gmail.com 의 ai 컨텐츠 생성 + AiCreation aiCreation1 = new AiCreation(null, users.get(1), CreationType.POETRY,LocalDate.of(2024,12,30), + LocalDate.of(2025, 1,5), "https://test/sample", "시입니다", CreationVisibility.PUBLIC); + AiCreation aiCreation2 = new AiCreation(null, users.get(1), CreationType.WRITING,LocalDate.of(2024,12,30), + LocalDate.of(2025, 1,5), "https://test/sample", "소설입니다", CreationVisibility.PUBLIC); + AiCreation aiCreation3 = new AiCreation(null, users.get(1), CreationType.POETRY,LocalDate.of(2025,1,6), + LocalDate.of(2025, 1,12), "https://test/sample", "시입니다", CreationVisibility.PUBLIC); + AiCreation aiCreation4 = new AiCreation(null, users.get(1), CreationType.WRITING,LocalDate.of(2025,1,6), + LocalDate.of(2025, 1,12), "https://test/sample", "소설입니다", CreationVisibility.PUBLIC); + AiCreation aiCreation5 = new AiCreation(null, users.get(1), CreationType.WRITING,LocalDate.of(2025,1,6), + LocalDate.of(2025, 1,12), "https://test/sample", "소설입니다", CreationVisibility.PRIVATE); + + aiCreationRepository.saveAll(Arrays.asList(aiCreation1, aiCreation2, aiCreation3, aiCreation4, aiCreation5)); + } + + @Test + void 공유_일기_가져오기() throws Exception{ + String jwtToken = jwtUtil.createJwt("user1@gmail.com", "ROLE_USER", 10000L); + //user1은 4개의 공유 일기를 가지고 있음 ( 2025-1-1 자유일기 2개, 2025-1-10 질문일기 1개 + 회고일기 1개 ) + ResultActions perform = mockMvc.perform(get("/api/shared/note") + .cookie(new Cookie("Authorization", jwtToken))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(4)) + .andExpect(jsonPath("$.sharedNotes[0].date").value("2025-01-10")) + .andExpect(jsonPath("$.sharedNotes[1].date").value("2025-01-01")) + .andExpect(jsonPath("$.sharedNotes[0].notes[0].type").value("QUESTION")) + .andExpect(jsonPath("$.sharedNotes[0].notes[1].type").value("RETROSPECTIVE")) + .andExpect(jsonPath("$.sharedNotes[1].notes[0].type").value("COMMON")) + .andExpect(jsonPath("$.sharedNotes[1].notes[1].type").value("COMMON")) + .andDo(print()); + } + + @Test + void 공유_일기_숏링크_가져오기() throws Exception{ + //user1은 4개의 공유 일기를 가지고 있음 ( 2025-1-1 자유일기 2개, 2025-1-10 질문일기 1개 + 회고일기 1개 ) + ResultActions perform = mockMvc.perform(get("/api/shortlink/note/user1hash")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(4)) + .andExpect(jsonPath("$.sharedNotes[0].date").value("2025-01-10")) + .andExpect(jsonPath("$.sharedNotes[1].date").value("2025-01-01")) + .andExpect(jsonPath("$.sharedNotes[0].notes[0].type").value("QUESTION")) + .andExpect(jsonPath("$.sharedNotes[0].notes[1].type").value("RETROSPECTIVE")) + .andExpect(jsonPath("$.sharedNotes[1].notes[0].type").value("COMMON")) + .andExpect(jsonPath("$.sharedNotes[1].notes[1].type").value("COMMON")) + .andDo(print()); + } + + @Test + void 공유_일기_한개도없을때() throws Exception{ + //user2는 0개의 공유 일기를 가지고 있음 + String jwtToken = jwtUtil.createJwt("user2@gmail.com", "ROLE_USER", 10000L); + ResultActions perform = mockMvc.perform(get("/api/shared/note") + .cookie(new Cookie("Authorization", jwtToken))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(0)) + .andDo(print()); + } + + @Test + void 공유_일기_숏링크_한개도없을때() throws Exception{ + //user2는 0개의 공유 일기를 가지고 있음 + ResultActions perform = mockMvc.perform(get("/api/shortlink/note/user2hash")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(0)) + .andDo(print()); + } + + @Test + void 공유_ai컨텐츠_가져오기() throws Exception{ + System.out.println("여기닷"); + List aiCreations = aiCreationRepository.findAll(); + for(AiCreation aiCreation : aiCreations){ + System.out.println(aiCreation.getUser().getUsername() +" " +aiCreation.getCreationVisibility()); + } + + String jwtToken = jwtUtil.createJwt("user2@gmail.com", "ROLE_USER", 10000L); + //user1은 4개의 공유 일기를 가지고 있음 ( 2025-1-1 자유일기 2개, 2025-1-10 질문일기 1개 + 회고일기 1개 ) + ResultActions perform = mockMvc.perform(get("/api/shared/ai") + .cookie(new Cookie("Authorization", jwtToken))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(4)) + .andExpect(jsonPath("$.sharedAiCreations[0].start_date").value("2024-12-30")) + .andExpect(jsonPath("$.sharedAiCreations[1].start_date").value("2025-01-06")) + .andExpect(jsonPath("$.sharedAiCreations[0].aiCreations[0].creationType").value("POETRY")) + .andExpect(jsonPath("$.sharedAiCreations[0].aiCreations[1].creationType").value("WRITING")) + .andExpect(jsonPath("$.sharedAiCreations[1].aiCreations[0].creationType").value("POETRY")) + .andExpect(jsonPath("$.sharedAiCreations[1].aiCreations[1].creationType").value("WRITING")) + .andDo(print()); + } + + @Test + void 공유_ai컨텐츠_숏링크_가져오기() throws Exception{ + //user1은 4개의 공유 일기를 가지고 있음 ( 2025-1-1 자유일기 2개, 2025-1-10 질문일기 1개 + 회고일기 1개 ) + ResultActions perform = mockMvc.perform(get("/api/shortlink/ai/user2hash")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(4)) + .andExpect(jsonPath("$.sharedAiCreations[0].start_date").value("2024-12-30")) + .andExpect(jsonPath("$.sharedAiCreations[1].start_date").value("2025-01-06")) + .andExpect(jsonPath("$.sharedAiCreations[0].aiCreations[0].creationType").value("POETRY")) + .andExpect(jsonPath("$.sharedAiCreations[0].aiCreations[1].creationType").value("WRITING")) + .andExpect(jsonPath("$.sharedAiCreations[1].aiCreations[0].creationType").value("POETRY")) + .andExpect(jsonPath("$.sharedAiCreations[1].aiCreations[1].creationType").value("WRITING")) + .andDo(print()); + } + @Test + void 공유_ai컨텐츠_한개도없을때() throws Exception{ + String jwtToken = jwtUtil.createJwt("user1@gmail.com", "ROLE_USER", 10000L); + //user1은 4개의 공유 일기를 가지고 있음 ( 2025-1-1 자유일기 2개, 2025-1-10 질문일기 1개 + 회고일기 1개 ) + ResultActions perform = mockMvc.perform(get("/api/shared/ai") + .cookie(new Cookie("Authorization", jwtToken))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(0)) + .andDo(print()); + } + + @Test + void 공유_ai컨텐츠_숏링크_한개도없을때() throws Exception{ + //user1은 4개의 공유 일기를 가지고 있음 ( 2025-1-1 자유일기 2개, 2025-1-10 질문일기 1개 + 회고일기 1개 ) + ResultActions perform = mockMvc.perform(get("/api/shortlink/ai/user1hash")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total").value(0)) + .andDo(print()); + } +} diff --git a/src/test/java/com/woozuda/backend/testutil/UserEntityBuilder.java b/src/test/java/com/woozuda/backend/testutil/UserEntityBuilder.java new file mode 100644 index 00000000..708d06c2 --- /dev/null +++ b/src/test/java/com/woozuda/backend/testutil/UserEntityBuilder.java @@ -0,0 +1,85 @@ +package com.woozuda.backend.testutil; + +import com.woozuda.backend.account.entity.AiType; +import com.woozuda.backend.account.entity.UserEntity; +import org.h2.engine.User; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + + +public class UserEntityBuilder { + + private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();; + private static final AtomicInteger COUNTER = new AtomicInteger(0); + + private Long id = null; + private String username; + private String password = "1234"; + private String role = "ROLE_USER"; + private AiType aiType = AiType.PICTURE_NOVEL; + private Boolean alarm = true; + private String email = "test@gmail.com"; + private String provider = "woozuda"; + + public UserEntityBuilder withUsername(String username) { + this.username = username; + return this; + } + + public UserEntityBuilder withPassword(String password) { + this.password = password; + return this; + } + + public UserEntityBuilder withRole(String role) { + this.role = role; + return this; + } + + public UserEntityBuilder withAiType(AiType aiType) { + this.aiType = aiType; + return this; + } + + public UserEntityBuilder withAlarm(Boolean alarm) { + this.alarm = alarm; + return this; + } + + public UserEntityBuilder withEmail(String email) { + this.email = email; + return this; + } + + public UserEntityBuilder withProvider(String provider) { + this.provider = provider; + return this; + } + + public UserEntity build() { + return new UserEntity(id, username, bCryptPasswordEncoder.encode(password), role, aiType, alarm, email, provider); + } + + public static void resetCounter() { + COUNTER.set(0); + } + public static UserEntityBuilder createUniqueUser(){ + int count = COUNTER.incrementAndGet(); + + UserEntityBuilder userEntityBuilder = new UserEntityBuilder(); + + userEntityBuilder.username = "user" + count + "@gmail.com"; + return userEntityBuilder; + } + + public static List createUniqueMultipleUser(int count){ + List userEntityList = new ArrayList<>(); + for(int i=0; i