diff --git a/.gradle/8.8/executionHistory/executionHistory.bin b/.gradle/8.8/executionHistory/executionHistory.bin index ddf3b7d..4ee6722 100644 Binary files a/.gradle/8.8/executionHistory/executionHistory.bin and b/.gradle/8.8/executionHistory/executionHistory.bin differ diff --git a/.gradle/8.8/executionHistory/executionHistory.lock b/.gradle/8.8/executionHistory/executionHistory.lock index 635c078..d8882f6 100644 Binary files a/.gradle/8.8/executionHistory/executionHistory.lock and b/.gradle/8.8/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.8/fileHashes/fileHashes.bin b/.gradle/8.8/fileHashes/fileHashes.bin index 88ddd07..51135a9 100644 Binary files a/.gradle/8.8/fileHashes/fileHashes.bin and b/.gradle/8.8/fileHashes/fileHashes.bin differ diff --git a/.gradle/8.8/fileHashes/fileHashes.lock b/.gradle/8.8/fileHashes/fileHashes.lock index 577954c..800fb0b 100644 Binary files a/.gradle/8.8/fileHashes/fileHashes.lock and b/.gradle/8.8/fileHashes/fileHashes.lock differ diff --git a/.gradle/8.8/fileHashes/resourceHashesCache.bin b/.gradle/8.8/fileHashes/resourceHashesCache.bin index a72909e..980ae37 100644 Binary files a/.gradle/8.8/fileHashes/resourceHashesCache.bin and b/.gradle/8.8/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 8093a43..97c9db0 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index 786162d..a06800a 100644 Binary files a/.gradle/file-system.probe and b/.gradle/file-system.probe differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleDataLoader.class.uniqueId2 b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleDataLoader.class.uniqueId2 deleted file mode 100644 index cbb1412..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleDataLoader.class.uniqueId2 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleRepository.class.uniqueId5 b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleRepository.class.uniqueId5 deleted file mode 100644 index 60d3324..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleRepository.class.uniqueId5 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl$ArticleServiceImplBuilder.class.uniqueId1 b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl$ArticleServiceImplBuilder.class.uniqueId1 deleted file mode 100644 index 184e0e4..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl$ArticleServiceImplBuilder.class.uniqueId1 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl$ArticleServiceImplBuilder.class.uniqueId3 b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl$ArticleServiceImplBuilder.class.uniqueId3 new file mode 100644 index 0000000..d0addf4 Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl$ArticleServiceImplBuilder.class.uniqueId3 differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl.class.uniqueId2 b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl.class.uniqueId2 new file mode 100644 index 0000000..176664d Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl.class.uniqueId2 differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl.class.uniqueId4 b/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl.class.uniqueId4 deleted file mode 100644 index 26df812..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/ArticleServiceImpl.class.uniqueId4 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ScrapController.class.uniqueId0 b/build/tmp/compileJava/compileTransaction/stash-dir/ScrapController.class.uniqueId0 index 7b0792b..a83099c 100644 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/ScrapController.class.uniqueId0 and b/build/tmp/compileJava/compileTransaction/stash-dir/ScrapController.class.uniqueId0 differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ScrapRepository.class.uniqueId1 b/build/tmp/compileJava/compileTransaction/stash-dir/ScrapRepository.class.uniqueId1 new file mode 100644 index 0000000..c9a4d95 Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/ScrapRepository.class.uniqueId1 differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/ScrapService.class.uniqueId3 b/build/tmp/compileJava/compileTransaction/stash-dir/ScrapService.class.uniqueId4 similarity index 56% rename from build/tmp/compileJava/compileTransaction/stash-dir/ScrapService.class.uniqueId3 rename to build/tmp/compileJava/compileTransaction/stash-dir/ScrapService.class.uniqueId4 index 6bb0722..9211ebe 100644 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/ScrapService.class.uniqueId3 and b/build/tmp/compileJava/compileTransaction/stash-dir/ScrapService.class.uniqueId4 differ diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin index c4dbf13..b864c6a 100644 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and b/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/src/main/java/com/winner_cat/domain/article/controller/ArticleController.java b/src/main/java/com/winner_cat/domain/article/controller/ArticleController.java index 24ce553..e0be491 100644 --- a/src/main/java/com/winner_cat/domain/article/controller/ArticleController.java +++ b/src/main/java/com/winner_cat/domain/article/controller/ArticleController.java @@ -30,25 +30,29 @@ public ResponseEntity> createArticle( // 게시글 수정 @PatchMapping("/{articleId}") public ResponseEntity> modifyArticle( - @PathVariable Long articleId, - @RequestBody ArticleUpdateDto.Req req) { - ResponseEntity> result = articleService.modifyArticle(articleId, req); + @PathVariable Long articleId, @RequestBody ArticleUpdateDto.Req req, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + String email = customUserDetails.getEmail(); + ResponseEntity> result = articleService.modifyArticle(articleId, req, email); return result; } // 게시글 삭제 @DeleteMapping("/{articleId}") public ResponseEntity> deleteArticle( - @PathVariable Long articleId) { - ResponseEntity> result = articleService.deleteArticle(articleId); + @PathVariable Long articleId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { + String email = customUserDetails.getEmail(); + ResponseEntity> result = articleService.deleteArticle(articleId, email); return result; } // 게시글 상세 조회 @GetMapping("/detail/{articleId}") public ResponseEntity> getArticleDetail( + @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable("articleId") Long articleId) { - ResponseEntity> result = articleService.getArticleDetail(articleId); + String email = userDetails.getEmail(); + ResponseEntity> result = articleService.getArticleDetail(email,articleId); return result; } @@ -82,4 +86,12 @@ public ResponseEntity getArticleRecommendByTag( Pageable pageable) { return articleService.getArticleRecommendByTag(tagName, pageable); } + + /** + * 오늘 모인 에러 개수 보여주기 + */ + @GetMapping("/today-error") + public ResponseEntity getTodayFixErrorInfo() { + return articleService.getTodayFixErrorInfo(); + } } \ No newline at end of file diff --git a/src/main/java/com/winner_cat/domain/article/dto/ArticleListDto.java b/src/main/java/com/winner_cat/domain/article/dto/ArticleListDto.java index e51d6f8..4014fdc 100644 --- a/src/main/java/com/winner_cat/domain/article/dto/ArticleListDto.java +++ b/src/main/java/com/winner_cat/domain/article/dto/ArticleListDto.java @@ -12,14 +12,14 @@ public class ArticleListDto { @NoArgsConstructor @AllArgsConstructor public static class ArticleResponse { - private Long id; + private String email; // 작성자 아이디 private String title; // 게시글 제목 @Builder.Default private List tags = new ArrayList<>(); // 태그 private String cause; // 원인 private String solution; // 해결 방법 private LocalDateTime updatedAt; - + private Boolean isScrapped; // 게시글 스크랩 유무 } // 게시글 조회 diff --git a/src/main/java/com/winner_cat/domain/article/dto/TodayErrorDto.java b/src/main/java/com/winner_cat/domain/article/dto/TodayErrorDto.java new file mode 100644 index 0000000..248fba8 --- /dev/null +++ b/src/main/java/com/winner_cat/domain/article/dto/TodayErrorDto.java @@ -0,0 +1,26 @@ +package com.winner_cat.domain.article.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +public class TodayErrorDto { + @Data + @Builder + public static class ErrorCount { + private Long totalCount; + @Builder.Default + private List ranking = new ArrayList<>(); + } + + @Data + @Builder + public static class ErrorDto { + private String tagName; + private Long count; + } +} + + diff --git a/src/main/java/com/winner_cat/domain/article/entity/ArticleTag.java b/src/main/java/com/winner_cat/domain/article/entity/ArticleTag.java index 65f3dc2..712383d 100644 --- a/src/main/java/com/winner_cat/domain/article/entity/ArticleTag.java +++ b/src/main/java/com/winner_cat/domain/article/entity/ArticleTag.java @@ -10,7 +10,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "ArticleTag") -public class ArticleTag { +public class ArticleTag{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "article_tag_id") diff --git a/src/main/java/com/winner_cat/domain/article/repository/ArticleRepository.java b/src/main/java/com/winner_cat/domain/article/repository/ArticleRepository.java index f7ed99a..4cbfe39 100644 --- a/src/main/java/com/winner_cat/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/winner_cat/domain/article/repository/ArticleRepository.java @@ -10,6 +10,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.List; @Repository @@ -22,4 +23,8 @@ public interface ArticleRepository extends JpaRepository { @Query("SELECT a FROM Article a JOIN a.tags t WHERE t.tag.tagName = :tagName ORDER BY a.createdAt DESC") Page
findByTagName(@Param("tagName") String tagName, Pageable pageable); -} \ No newline at end of file + + // 오늘 작성된 게시글 찾기 + @Query("SELECT a FROM Article a WHERE a.createdAt >= :startTime AND a.createdAt <= :endTime") + List
findAllByCreatedAtBetween(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); +} diff --git a/src/main/java/com/winner_cat/domain/article/repository/ArticleTagRepository.java b/src/main/java/com/winner_cat/domain/article/repository/ArticleTagRepository.java index 223c3d6..7ce9363 100644 --- a/src/main/java/com/winner_cat/domain/article/repository/ArticleTagRepository.java +++ b/src/main/java/com/winner_cat/domain/article/repository/ArticleTagRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -13,5 +15,11 @@ public interface ArticleTagRepository extends JpaRepository { void deleteByArticle(Article article); List findByArticle(Article article); List findByTag(Tag tag); + + @Query("SELECT at FROM ArticleTag at JOIN at.article a WHERE at.tag = :tag ORDER BY a.createdAt DESC") Page findArticleTagPageByTag(Tag tag, Pageable pageable); + + // 태그 이름으로 묶어서 내림차순 추출 + @Query("SELECT at.tag.tagName, COUNT(at) FROM ArticleTag at WHERE at.article IN :articles GROUP BY at.tag.tagName ORDER BY COUNT(at) DESC") + List findTopTagsByArticles(@Param("articles") List
articles); } diff --git a/src/main/java/com/winner_cat/domain/article/service/ArticleService.java b/src/main/java/com/winner_cat/domain/article/service/ArticleService.java index 04f1715..be05e55 100644 --- a/src/main/java/com/winner_cat/domain/article/service/ArticleService.java +++ b/src/main/java/com/winner_cat/domain/article/service/ArticleService.java @@ -10,11 +10,12 @@ public interface ArticleService { ResponseEntity> createArticle(ArticleCreateDto.Req req,String email); - ResponseEntity> modifyArticle(Long articleId, ArticleUpdateDto.Req req); - ResponseEntity> deleteArticle(Long articleId); - ResponseEntity> getArticleDetail(Long articleId ); + ResponseEntity> getArticleDetail(String email, Long articleId); + ResponseEntity> modifyArticle(Long articleId, ArticleUpdateDto.Req req, String email); + ResponseEntity> deleteArticle(Long articleId, String email); ResponseEntity> getMyArticles(String email, Pageable pageable); ResponseEntity getAllArticle(Pageable pageable); ResponseEntity getArticleByTag(String tagName, Pageable pageable); ResponseEntity getArticleRecommendByTag(String tagName, Pageable pageable); + ResponseEntity getTodayFixErrorInfo(); } diff --git a/src/main/java/com/winner_cat/domain/article/service/ArticleServiceImpl.java b/src/main/java/com/winner_cat/domain/article/service/ArticleServiceImpl.java index 434af3c..a636470 100644 --- a/src/main/java/com/winner_cat/domain/article/service/ArticleServiceImpl.java +++ b/src/main/java/com/winner_cat/domain/article/service/ArticleServiceImpl.java @@ -9,19 +9,23 @@ import com.winner_cat.domain.article.repository.TagRepository; import com.winner_cat.domain.member.entity.Member; import com.winner_cat.domain.member.repository.MemberRepository; +import com.winner_cat.domain.scrap.repository.ScrapRepository; import com.winner_cat.global.enums.statuscode.ErrorStatus; import com.winner_cat.global.exception.GeneralException; import com.winner_cat.global.response.ApiResponse; -import jakarta.transaction.Transactional; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; @Service @@ -33,6 +37,7 @@ public class ArticleServiceImpl implements ArticleService{ private final ArticleRepository articleRepository; private final TagRepository tagRepository; private final ArticleTagRepository articleTagRepository; + private final ScrapRepository scrapRepository; /** * 게시글 작성 @@ -80,11 +85,16 @@ public ResponseEntity> createArticle(ArticleCreateDto.Req req, St * - 게시글 작성자와 현재 로그인한 사용자가 같은 사용자인지 확인하는 작업 작성 필요 */ @Override - public ResponseEntity> modifyArticle(Long articleId, ArticleUpdateDto.Req req) { + public ResponseEntity> modifyArticle(Long articleId, ArticleUpdateDto.Req req, String email) { // 게시물 검색 Article article = articleRepository.findById(articleId) .orElseThrow(() -> new GeneralException(ErrorStatus.ARTICLE_NOT_FOUND)); + // 게시글 작성자와 로그인한 사용자의 이메일 비교 + if (!article.getAuthor().getEmail().equals(email)) { + throw new GeneralException(ErrorStatus.ARTICLE_MEMBER_NOT_FOUND); + } + // 게시물 업데이트 articleTagRepository.deleteByArticle(article); // 기존 게시글 삭제 @@ -125,11 +135,19 @@ public ResponseEntity> modifyArticle(Long articleId, ArticleUpdat * 내가 작성한 게시글만 삭제가 가능하다 */ @Override - public ResponseEntity> deleteArticle(Long articleId){ + public ResponseEntity> deleteArticle(Long articleId, String email){ // 게시물 검색 Article article = articleRepository.findById(articleId) .orElseThrow(() -> new GeneralException(ErrorStatus.ARTICLE_NOT_FOUND)); + // 게시글 작성자와 요청자의 이메일 비교 + if (!article.getAuthor().getEmail().equals(email)) { + throw new GeneralException(ErrorStatus.ARTICLE_MEMBER_NOT_FOUND); + } + + // 스크랩 정보 삭제 + scrapRepository.deleteByArticle(article); + // 연관관계 매핑 제거 articleTagRepository.deleteByArticle(article); @@ -144,13 +162,17 @@ public ResponseEntity> deleteArticle(Long articleId){ * 게시글 상세 보기 */ @Override - public ResponseEntity> getArticleDetail(Long articleId) { + public ResponseEntity> getArticleDetail(String email,Long articleId) { Article article = articleRepository.findById(articleId) .orElseThrow(() -> new GeneralException(ErrorStatus.ARTICLE_NOT_FOUND)); + // 현재 로그인 중인 사용자 + Member member = memberRepository.findMemberByEmail(email) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); List tagResponseDtoList = new ArrayList<>(); List articleTagsList = articleTagRepository.findByArticle(article); + // 태그 정보 추출 for (ArticleTag articleTag : articleTagsList) { Tag tag = articleTag.getTag(); tagResponseDtoList.add( @@ -160,14 +182,16 @@ public ResponseEntity> getArticleDetail(Long articleId) { .build() ); } - - + // 스크랩 유무 + boolean isScrapped = scrapRepository.existsByMemberAndArticle(member, article); ArticleListDto.ArticleResponse articleResponse = ArticleListDto.ArticleResponse.builder() + .email(article.getAuthor().getEmail()) // 게시글 작성자의 이메일 .title(article.getTitle()) .tags(tagResponseDtoList) .cause(article.getCause()) .solution(article.getSolution()) .updatedAt(article.getUpdatedAt()) + .isScrapped(isScrapped) .build(); return ResponseEntity.ok().body(ApiResponse.onSuccess(articleResponse)); @@ -217,10 +241,20 @@ public ResponseEntity> getMyArticles(String email, Pageable pagea return ResponseEntity.ok().body(ApiResponse.onSuccess(result)); } + /** + * 전체 게시글 조회(미리보기) + */ @Override public ResponseEntity getAllArticle(Pageable pageable) { + // 기본 정렬 기준 설정: createdAt 속성 기준으로 내림차순 정렬 + Pageable defaultPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + Sort.by(Sort.Direction.DESC, "createdAt") + ); + // 1. pageable 객체를 바탕으로 전체 게시글 엔티티 조회 - Page
articlePage = articleRepository.findAll(pageable); + Page
articlePage = articleRepository.findAll(defaultPageable); int totalPages = articlePage.getTotalPages(); // 2. 반환 DTO 생성 및 반환 List resultDtoList = new ArrayList<>(); @@ -264,15 +298,14 @@ public ResponseEntity getArticleByTag(String tagName, Pageable pageable) { List resultDtoList = new ArrayList<>(); for (ArticleTag articleTag : articleTagList) { Article article = articleTag.getArticle(); - List tagResponseDtoList = new ArrayList<>(); // 관련 태그들 얻어오기 - Tag tag = articleTag.getTag(); - tagResponseDtoList.add( - TagResponseDto.builder() - .tagName(tag.getTagName()) - .colorCode(tag.getColorCode()) - .build() - ); + List tagResponseDtoList = article.getTags().stream() + .map(at -> TagResponseDto.builder() + .tagName(at.getTag().getTagName()) + .colorCode(at.getTag().getColorCode()) + .build()) + .collect(Collectors.toList()); + ArticlePreviewDto.AllArticlePreview result = ArticlePreviewDto.AllArticlePreview .builder() .articleId(article.getId()) @@ -318,4 +351,33 @@ public ResponseEntity getArticleRecommendByTag(String tagName, Pageable pagea .build(); return ResponseEntity.ok().body(ApiResponse.onSuccess(result)); } + + /** + * 오늘 해결한 총 에러와, 상위 태그 4개 반환 + */ + @Override + @Transactional(readOnly = true) + public ResponseEntity getTodayFixErrorInfo() { + LocalDateTime endTime = LocalDateTime.now(); + LocalDateTime startTime = endTime.toLocalDate().atStartOfDay(); + List
todayArticles = articleRepository.findAllByCreatedAtBetween(startTime, endTime); + long totalCount = todayArticles.size(); + + // 가장 많이 작성된 태그 순으로 정렬 + List topTags = articleTagRepository.findTopTagsByArticles(todayArticles); + // 반환 DTO 생성 + List top4Articles = new ArrayList<>(); + topTags.stream().limit(4).forEach(tag -> { + top4Articles.add(TodayErrorDto.ErrorDto.builder() + .tagName((String) tag[0]) + .count(((Number) tag[1]).longValue()) + .build()); + }); + TodayErrorDto.ErrorCount result = TodayErrorDto.ErrorCount.builder() + .totalCount(totalCount) + .ranking(top4Articles) + .build(); + + return ResponseEntity.ok(ApiResponse.onSuccess(result)); + } } diff --git a/src/main/java/com/winner_cat/domain/questionroom/controller/QuestionRoomController.java b/src/main/java/com/winner_cat/domain/questionroom/controller/QuestionRoomController.java index 49fb7d5..266ce16 100644 --- a/src/main/java/com/winner_cat/domain/questionroom/controller/QuestionRoomController.java +++ b/src/main/java/com/winner_cat/domain/questionroom/controller/QuestionRoomController.java @@ -1,5 +1,6 @@ package com.winner_cat.domain.questionroom.controller; +import com.winner_cat.domain.questionroom.dto.AdoptAnswerDto; import com.winner_cat.domain.questionroom.dto.ChangeQuestionRoomStateDTO; import com.winner_cat.domain.questionroom.service.QuestionRoomService; import com.winner_cat.global.jwt.dto.CustomUserDetails; @@ -13,6 +14,7 @@ @RequiredArgsConstructor public class QuestionRoomController { private final QuestionRoomService questionRoomService; + /** * 질문방 미리보기 * 특정 회원이 생성한 질문방의 제목과, 답변 상태, 최종 수정 시간을 반환한다. @@ -35,7 +37,7 @@ public ResponseEntity getQuestionRoomDetail( } /** - * 질문방 상태 변경 + * 질문방 상태 변경(답변 외 방법으로 해결하였습니다.) */ @PatchMapping("/state") public ResponseEntity changeState( @@ -43,4 +45,13 @@ public ResponseEntity changeState( return questionRoomService .changeState(requestDto.getQuestionRoomId(), requestDto.getState()); } + + /** + * 답변 채택하기 + */ + @PatchMapping("/adopt") + public ResponseEntity adoptAnswer(@RequestBody AdoptAnswerDto requestDto) { + return questionRoomService.adoptAnswer(requestDto); + } + } diff --git a/src/main/java/com/winner_cat/domain/questionroom/dto/AdoptAnswerDto.java b/src/main/java/com/winner_cat/domain/questionroom/dto/AdoptAnswerDto.java new file mode 100644 index 0000000..193a545 --- /dev/null +++ b/src/main/java/com/winner_cat/domain/questionroom/dto/AdoptAnswerDto.java @@ -0,0 +1,11 @@ +package com.winner_cat.domain.questionroom.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AdoptAnswerDto { + private Long questionRoomId; // 채택하려는 질문방의 아이디 + private Long answerId; // 채택하려는 답변의 아이디 +} diff --git a/src/main/java/com/winner_cat/domain/questionroom/service/QuestionRoomService.java b/src/main/java/com/winner_cat/domain/questionroom/service/QuestionRoomService.java index a64cd9a..b9a6c64 100644 --- a/src/main/java/com/winner_cat/domain/questionroom/service/QuestionRoomService.java +++ b/src/main/java/com/winner_cat/domain/questionroom/service/QuestionRoomService.java @@ -4,10 +4,7 @@ import com.winner_cat.domain.article.entity.Article; import com.winner_cat.domain.member.entity.Member; import com.winner_cat.domain.member.repository.MemberRepository; -import com.winner_cat.domain.questionroom.dto.AnswerDto; -import com.winner_cat.domain.questionroom.dto.CheckChattingRoomDetailDto; -import com.winner_cat.domain.questionroom.dto.QuestionDto; -import com.winner_cat.domain.questionroom.dto.QuestionRoomListDto; +import com.winner_cat.domain.questionroom.dto.*; import com.winner_cat.domain.questionroom.entity.Answer; import com.winner_cat.domain.questionroom.entity.Question; import com.winner_cat.domain.questionroom.entity.QuestionRoom; @@ -17,6 +14,8 @@ import com.winner_cat.domain.questionroom.repository.QuestionRoomRepository; import com.winner_cat.global.enums.statuscode.ErrorStatus; import com.winner_cat.global.exception.GeneralException; +import com.winner_cat.global.gpt.dto.ChatCompletionDto; +import com.winner_cat.global.gpt.dto.ChatRequestMsgDto; import com.winner_cat.global.jwt.dto.CustomUserDetails; import com.winner_cat.global.response.ApiResponse; import lombok.Builder; @@ -38,6 +37,7 @@ public class QuestionRoomService { private final QuestionRepository questionRepository; private final AnswerRepository answerRepository; private final MemberRepository memberRepository; + private final QuestionService questionService; /** * 질문 방 상태 수정 @@ -114,4 +114,45 @@ public ResponseEntity getQuestionRoomPreview(String email) { return ResponseEntity.ok().body(ApiResponse.onSuccess(questionRoomResponses)); } + + /** + * 답변 채택하기 + * 1. 채택한 답변 3줄로 요약하여 반환 + * 2. 질문방의 상태 DONE으로 변경 + */ + public ResponseEntity adoptAnswer(AdoptAnswerDto requestDto) { + Long questionRoomId = requestDto.getQuestionRoomId(); + + QuestionRoom questionRoom = questionRoomRepository.findById(questionRoomId) + .orElseThrow(() -> new GeneralException(ErrorStatus.QUESTION_ROOM_NOT_FOUND)); + + Long answerId = requestDto.getAnswerId(); + + Answer answer = answerRepository.findById(answerId) + .orElseThrow(() -> new GeneralException(ErrorStatus.ANSWER_NOT_FOUND)); + + String answerContent = answer.getContent(); + + // 1. 답변 요약 + + List messages = new ArrayList<>(); + String prompt = "문제 해결 방법에 대한 글을 작성하고 싶습니다. 어떻게 오류를 해결했는지 아래 내용을 3줄 이하로 짧게 요약하세요."; + messages.add(new ChatRequestMsgDto("system", prompt)); + messages.add(new ChatRequestMsgDto("user", answerContent)); + + ChatCompletionDto chatCompletionDto = ChatCompletionDto.builder() + .model("gpt-4o") + .messages(messages) + .build(); + +// System.out.println("===== 요청 본문 ====="); +// messages.forEach(msg -> System.out.println(msg.getRole() + ": " + msg.getContent())); +// System.out.println("===================="); + + String result = questionService.callGptApi(chatCompletionDto); + + // 2. 질문방 상태 완료로 변경 + questionRoom.changeState(QuestionState.DONE); + return ResponseEntity.ok(ApiResponse.onSuccess(result)); + } } diff --git a/src/main/java/com/winner_cat/domain/questionroom/service/QuestionService.java b/src/main/java/com/winner_cat/domain/questionroom/service/QuestionService.java index 1b76f97..dfc13f2 100644 --- a/src/main/java/com/winner_cat/domain/questionroom/service/QuestionService.java +++ b/src/main/java/com/winner_cat/domain/questionroom/service/QuestionService.java @@ -123,9 +123,9 @@ public ResponseEntity startNewQuestion(String email, String question) { .build(); String gptAnswer = callGptApi(chatCompletionDto); - System.out.println("==============="); - System.out.println("gptAnswer = " + gptAnswer); - System.out.println("==============="); +// System.out.println("==============="); +// System.out.println("gptAnswer = " + gptAnswer); +// System.out.println("==============="); // 제목과 답변으로 내용 분리 // 정규표현식 패턴 정의 String regex = "\\[(.*?)\\]\\s*(.*)"; @@ -145,10 +145,10 @@ public ResponseEntity startNewQuestion(String email, String question) { // 분리 후 질문방에 제목 지정 savedQuestionRoom.changeName(title); - System.out.println("=============="); - System.out.println("title = " + title); - System.out.println("content = " + content); - System.out.println("=============="); +// System.out.println("=============="); +// System.out.println("title = " + title); +// System.out.println("content = " + content); +// System.out.println("=============="); // 3. 새로운 사용자 질문 저장, 질문에 대한 답변 저장 Question questionEntity = Question.builder() @@ -197,7 +197,7 @@ private List createMessagesFromHistory(List questio return messages; } - private String callGptApi(ChatCompletionDto chatCompletionDto) { + public String callGptApi(ChatCompletionDto chatCompletionDto) { try { HttpClient client = HttpClient.newHttpClient(); @@ -215,8 +215,8 @@ private String callGptApi(ChatCompletionDto chatCompletionDto) { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // 응답 형태 확인 -// System.out.println("GPT 응답 상태 코드: " + response.statusCode()); -// System.out.println("GPT 응답 본문: " + response.body()); + System.out.println("GPT 응답 상태 코드: " + response.statusCode()); + System.out.println("GPT 응답 본문: " + response.body()); if (response.statusCode() == 200) { JsonNode jsonNode = objectMapper.readTree(response.body()); diff --git a/src/main/java/com/winner_cat/domain/scrap/controller/ScrapController.java b/src/main/java/com/winner_cat/domain/scrap/controller/ScrapController.java index 62bdb47..34b68d8 100644 --- a/src/main/java/com/winner_cat/domain/scrap/controller/ScrapController.java +++ b/src/main/java/com/winner_cat/domain/scrap/controller/ScrapController.java @@ -32,6 +32,17 @@ public ResponseEntity scrapArticle( @GetMapping("/mine") public ResponseEntity getAllMyScrapArticles( @AuthenticationPrincipal CustomUserDetails userDetails, Pageable pageable) { - return scrapService.getAllMyScrapArticles(userDetails.getEmail(),pageable); + return scrapService.getAllMyScrapArticles(userDetails.getEmail(), pageable); + } + + /** + * 스크랩 취소 + */ + @DeleteMapping("/cancel/{articleId}") + public ResponseEntity cancelScrap( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable(name = "articleId") Long articleId) { + String email = userDetails.getEmail(); + return scrapService.cancelScrap(email, articleId); } } diff --git a/src/main/java/com/winner_cat/domain/scrap/repository/ScrapRepository.java b/src/main/java/com/winner_cat/domain/scrap/repository/ScrapRepository.java index a7da8e3..73c5339 100644 --- a/src/main/java/com/winner_cat/domain/scrap/repository/ScrapRepository.java +++ b/src/main/java/com/winner_cat/domain/scrap/repository/ScrapRepository.java @@ -1,12 +1,22 @@ package com.winner_cat.domain.scrap.repository; +import com.winner_cat.domain.article.entity.Article; import com.winner_cat.domain.member.entity.Member; import com.winner_cat.domain.scrap.entity.Scrap; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; public interface ScrapRepository extends JpaRepository { + + @Query("SELECT sc FROM Scrap sc JOIN sc.article ac WHERE sc.member = :member ORDER BY ac.createdAt DESC") Page findByMember(Member member, Pageable pageable); + // 회원이 게시글을 스크랩했는지 유무 + boolean existsByMemberAndArticle(Member member, Article article); + Optional findScrapInfoByMemberAndArticle(Member member, Article article); + void deleteByArticle(Article article); } diff --git a/src/main/java/com/winner_cat/domain/scrap/service/ScrapService.java b/src/main/java/com/winner_cat/domain/scrap/service/ScrapService.java index f2346b1..de77c91 100644 --- a/src/main/java/com/winner_cat/domain/scrap/service/ScrapService.java +++ b/src/main/java/com/winner_cat/domain/scrap/service/ScrapService.java @@ -91,4 +91,22 @@ public ResponseEntity getAllMyScrapArticles(String email, Pageable pageable) return ResponseEntity.ok().body(ApiResponse.onSuccess(result)); } + + /** + * 스크랩 취소 + */ + public ResponseEntity cancelScrap(String email, Long articleId) { + // 1. 회원 정보 조회 + Member member = memberRepository.findMemberByEmail(email) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + // 2. 게시글 정보 조회 + Article article = articleRepository.findById(articleId) + .orElseThrow(() -> new GeneralException(ErrorStatus.ARTICLE_NOT_FOUND)); + // 3. 스크랩 정보 조회 + Scrap scrapInfo = scrapRepository.findScrapInfoByMemberAndArticle(member, article) + .orElseThrow(() -> new GeneralException(ErrorStatus.SCRAPINFO_NOT_FOUND)); + // 4. 스크랩 정보 삭제 + scrapRepository.delete(scrapInfo); + return ResponseEntity.ok(ApiResponse.onSuccess("스크랩이 취소되었습니다.")); + } } diff --git a/src/main/java/com/winner_cat/global/config/PermitAllPathsConfig.java b/src/main/java/com/winner_cat/global/config/PermitAllPathsConfig.java index bdaf3d1..a610ad4 100644 --- a/src/main/java/com/winner_cat/global/config/PermitAllPathsConfig.java +++ b/src/main/java/com/winner_cat/global/config/PermitAllPathsConfig.java @@ -10,8 +10,9 @@ public class PermitAllPathsConfig { public String[] permitAllPaths() { return new String[]{ "/login", "/join", "/oauth2/**", "/ci", - "/api/article/today-error", "/api/scream/**", "/api/article/all", - "/api/article/detail/**", "/api/article/tag/**" + "/api/article/today-error", "/api/scream/**", + "/api/article/all", "/api/article/tag/**", + "/api/article/today-error", "/api/article/recommend/**" }; } } \ No newline at end of file diff --git a/src/main/java/com/winner_cat/global/config/SecurityConfig.java b/src/main/java/com/winner_cat/global/config/SecurityConfig.java index a3bda83..46e3bde 100644 --- a/src/main/java/com/winner_cat/global/config/SecurityConfig.java +++ b/src/main/java/com/winner_cat/global/config/SecurityConfig.java @@ -53,8 +53,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); + configuration.setAllowedOriginPatterns(Collections.singletonList("*")); + //configuration.setAllowedOrigins(Collections.singletonList("*")); configuration.setAllowedMethods(Collections.singletonList("*")); configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Collections.singletonList("*")); diff --git a/src/main/java/com/winner_cat/global/enums/statuscode/ErrorStatus.java b/src/main/java/com/winner_cat/global/enums/statuscode/ErrorStatus.java index 5438cb7..13cb8d6 100644 --- a/src/main/java/com/winner_cat/global/enums/statuscode/ErrorStatus.java +++ b/src/main/java/com/winner_cat/global/enums/statuscode/ErrorStatus.java @@ -26,6 +26,7 @@ public enum ErrorStatus implements BaseCode { // Article ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "삭제되었거나 존재하지 않는 게시글입니다."), + ARTICLE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4002", "게시글 작성자만 접근 가능합니다."), // Tag TAG_NOT_FOUND(HttpStatus.NOT_FOUND, "TAG4001", "해당하는 태그가 없습니다"), @@ -40,7 +41,12 @@ public enum ErrorStatus implements BaseCode { // Question QUESTION_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "QUESTIONROOM4001", "존재하지 않는 질문방 입니다."), - FAIL_TO_CREATE_ANSWER(HttpStatus.NOT_FOUND, "QUESTIONROOM4002", "답변을 생성하는데 실패하였습니다. GPT API키가 만료되었을 수 있습니다."); + FAIL_TO_CREATE_ANSWER(HttpStatus.NOT_FOUND, "QUESTIONROOM4002", "답변을 생성하는데 실패하였습니다. GPT API키가 만료되었을 수 있습니다."), + // Scrap + SCRAPINFO_NOT_FOUND(HttpStatus.NOT_FOUND, "SCRAP4001", "스크랩 정보가 없습니다. 아직 스크랩하지 않은 게시글입니다."), + + // Answer + ANSWER_NOT_FOUND(HttpStatus.NOT_FOUND, "ANSWER4001", "해당하는 답변이 존재하지 않습니다. 질문 아이디를 다시 한번 확인해주세요"); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/winner_cat/global/oauth2/handler/CustomSuccessHandler.java b/src/main/java/com/winner_cat/global/oauth2/handler/CustomSuccessHandler.java index 1ebdb2f..d0c1964 100644 --- a/src/main/java/com/winner_cat/global/oauth2/handler/CustomSuccessHandler.java +++ b/src/main/java/com/winner_cat/global/oauth2/handler/CustomSuccessHandler.java @@ -51,7 +51,7 @@ public void onAuthenticationSuccess( logger.info("JWT Token Created and Added to Response"); - String targetUrl = "http://localhost:3000/main"; // 메인 페이지로 리다이렉트 + String targetUrl = "https://bugnyang2.netlify.app/"; // 메인 페이지로 리다이렉트 getRedirectStrategy().sendRedirect(request, response, targetUrl); logger.info("Redirected to: " + targetUrl); diff --git a/src/main/java/com/winner_cat/init/article/ArticleDataLoader.java b/src/main/java/com/winner_cat/init/article/ArticleDataLoader.java index a1a2f25..bb6baef 100644 --- a/src/main/java/com/winner_cat/init/article/ArticleDataLoader.java +++ b/src/main/java/com/winner_cat/init/article/ArticleDataLoader.java @@ -8,21 +8,14 @@ import com.winner_cat.domain.article.repository.TagRepository; import com.winner_cat.domain.member.entity.Member; import com.winner_cat.domain.member.repository.MemberRepository; -import com.winner_cat.domain.questionroom.entity.Answer; -import com.winner_cat.domain.questionroom.entity.Question; -import com.winner_cat.domain.questionroom.entity.QuestionRoom; -import com.winner_cat.domain.questionroom.entity.enums.QuestionState; -import com.winner_cat.domain.questionroom.repository.AnswerRepository; -import com.winner_cat.domain.questionroom.repository.QuestionRepository; -import com.winner_cat.domain.questionroom.repository.QuestionRoomRepository; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; - -@Component +//@Component +//@Order(2) @RequiredArgsConstructor public class ArticleDataLoader implements CommandLineRunner { @@ -34,13 +27,19 @@ public class ArticleDataLoader implements CommandLineRunner { @Override public void run(String... args) throws Exception { - // 회원 생성 - Member member1 = createMember("username1","testuser@example.com","1234", "USER1", "ROLE_ADMIN" ); - Member member2 = createMember("username2","testuser2@example.com","1234", "USER2", "ROLE_USER"); +// Member member1 = createMember("username1","testuser@example.com","1234", "USER1", "ROLE_ADMIN" ); +// Member member2 = createMember("username2","testuser2@example.com","1234", "USER2", "ROLE_USER"); + Member member1 = memberRepository.findByUsername("username1"); + Member member2 = memberRepository.findByUsername("username2"); // 태그 생성 - Tag tagJava = createTag("java","#FF3F3F"); - Tag tagSwift = createTag("swift","#6630ff"); + Tag tagJava = tagRepository.findByTagName("Java").get(); + Tag tagSwift = tagRepository.findByTagName("Swift").get(); + Tag tagiOS = tagRepository.findByTagName("iOS").get(); + Tag tagPython = tagRepository.findByTagName("Python").get(); + Tag tagAndroid = tagRepository.findByTagName("Android").get(); + Tag tagKotlin = tagRepository.findByTagName("Kotlin").get(); + Tag tagFlask = tagRepository.findByTagName("Flask").get(); // 게시글 생성 Article member1Article1 @@ -51,17 +50,65 @@ public void run(String... args) throws Exception { = createArticle("제목2", "원인2", "해결법2", member2); Article member2Article2 = createArticle("제목22", "원인22", "해결법22", member2); + Article member2Article3 + = createArticle("제목3", "원인3", "해결법3", member2); + Article member2Article4 + = createArticle("제목33", "원인33", "해결법33", member2); + Article member2Article5 + = createArticle("제목4", "원인4", "해결법4", member1); + Article member2Article6 + = createArticle("제목44", "원인44", "해결법44", member2); + Article member2Article7 + = createArticle("제목7", "원인7", "해결법3", member2); + Article member2Article8 + = createArticle("제목77", "원인77", "해결법33", member2); + Article member2Article9 + = createArticle("제목8", "원인8", "해결법4", member2); + Article member2Article10 + = createArticle("제목88", "원인88", "해결법44", member1); + Article member2Article11 + = createArticle("제목9", "원인9", "해결법3", member1); + Article member2Article12 + = createArticle("제목99", "원인99", "해결법33", member2); + Article member2Article13 + = createArticle("제목10", "원인10", "해결법4", member1); + Article member2Article14 + = createArticle("제목1010", "원인1010", "해결법44", member2); // 게시글 - 태그 연관관계 설정 - setTagToArticle(member1Article1,tagJava); + setTagToArticle(member1Article1,tagiOS); setTagToArticle(member1Article1,tagSwift); setTagToArticle(member1Article2,tagJava); - setTagToArticle(member2Article1,tagJava); + setTagToArticle(member2Article1,tagiOS); setTagToArticle(member2Article1,tagSwift); setTagToArticle(member2Article2,tagSwift); + + setTagToArticle(member2Article3,tagiOS); + setTagToArticle(member2Article3,tagSwift); + + setTagToArticle(member2Article4,tagPython); + + setTagToArticle(member2Article5,tagPython); + setTagToArticle(member2Article5,tagSwift); + setTagToArticle(member2Article5,tagiOS); + + setTagToArticle(member2Article6,tagPython); + setTagToArticle(member2Article7,tagPython); + setTagToArticle(member2Article7,tagFlask); + + setTagToArticle(member2Article8,tagFlask); + + setTagToArticle(member2Article9,tagAndroid); + setTagToArticle(member2Article10,tagKotlin); + setTagToArticle(member2Article11,tagiOS); + setTagToArticle(member2Article12,tagiOS); + setTagToArticle(member2Article13,tagKotlin); + setTagToArticle(member2Article14,tagPython); + + } public Member createMember(String username, String email, String password, String nickname, String role) { diff --git a/src/main/java/com/winner_cat/init/questionroom/DataLoader.java b/src/main/java/com/winner_cat/init/questionroom/DataLoader.java index 1d6f799..6f2df53 100644 --- a/src/main/java/com/winner_cat/init/questionroom/DataLoader.java +++ b/src/main/java/com/winner_cat/init/questionroom/DataLoader.java @@ -1,5 +1,7 @@ package com.winner_cat.init.questionroom; +import com.winner_cat.domain.member.entity.Member; +import com.winner_cat.domain.member.repository.MemberRepository; import com.winner_cat.domain.questionroom.entity.Answer; import com.winner_cat.domain.questionroom.entity.Question; import com.winner_cat.domain.questionroom.entity.QuestionRoom; @@ -9,26 +11,37 @@ import com.winner_cat.domain.questionroom.repository.QuestionRoomRepository; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.time.LocalDateTime; -@Component +//@Component +//@Order(3) @RequiredArgsConstructor public class DataLoader implements CommandLineRunner { private final QuestionRoomRepository questionRoomRepository; private final QuestionRepository questionRepository; private final AnswerRepository answerRepository; + private final MemberRepository memberRepository; @Override public void run(String... args) throws Exception { + Member username1 = memberRepository.findByUsername("username1"); + Member username2 = memberRepository.findByUsername("username2"); // 질문방 생성 QuestionRoom room1 = questionRoomRepository.save( - QuestionRoom.builder().state(QuestionState.PROGRESS).build() + QuestionRoom.builder() + .state(QuestionState.PROGRESS) + .member(username1) + .build() ); QuestionRoom room2 = questionRoomRepository.save( - QuestionRoom.builder().state(QuestionState.PROGRESS).build() + QuestionRoom.builder() + .state(QuestionState.PROGRESS) + .member(username2) + .build() ); // 질문과 답변 생성 (room1) diff --git a/src/main/java/com/winner_cat/init/tag/TagDataLoader.java b/src/main/java/com/winner_cat/init/tag/TagDataLoader.java new file mode 100644 index 0000000..b0c8bc5 --- /dev/null +++ b/src/main/java/com/winner_cat/init/tag/TagDataLoader.java @@ -0,0 +1,48 @@ +package com.winner_cat.init.tag; + +import com.winner_cat.domain.article.entity.Tag; +import com.winner_cat.domain.article.repository.TagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +//@Component +//@Order(1) +@RequiredArgsConstructor +public class TagDataLoader implements CommandLineRunner { + private final TagRepository tagRepository; + + @Override + public void run(String... args) throws Exception { + createTag("JavaScript", "#F7DF1E"); + createTag("Python", "#306998"); + createTag("Java", "#5382A1"); + createTag("C#", "#239120"); + createTag("C/C++", "#00599C"); + createTag("Swift", "#FA7343"); + createTag("Kotlin", "#0095D5"); + createTag("TypeScript", "#3178C6"); + createTag("React", "#61DAFB"); + createTag("Angular", "#DD0031"); + createTag("Vue.js", "#4FC08D"); + createTag("Django", "#092E20"); + createTag("Flask", "#000000"); + createTag("Spring", "#6DB33F"); + createTag("Express", "#000000"); + createTag("NestJS", "#E0234E"); + createTag("iOS", "#A2AAAD"); + createTag("Android", "#3DDC84"); + createTag("React Native", "#137CBD"); + createTag("Flutter", "#02569B"); + createTag("SQL", "#00758F"); + } + + public Tag createTag(String tagName, String colorCode) { + Tag tag = Tag.builder() + .tagName(tagName) + .colorCode(colorCode) + .build(); + return tagRepository.save(tag); + } +} \ No newline at end of file