From 7409899d2d1b7c04a6152c4ec42102db913b59c2 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Mon, 2 Dec 2024 10:24:05 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[add]=20=ED=94=84=EB=A1=A0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20url=20=EC=A3=BC=EC=86=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/service/ChatRoomService.java | 7 +- .../controller/MemberUserController.java | 24 +- .../member/repository/MemberRepository.java | 16 +- .../member/service/MemberAdminService.java | 6 +- .../com/mallangs/global/config/MvcConfig.java | 1 + .../global/config/SecurityConfig.java | 2 +- .../repository/ArticleRepositoryTest.java | 2 + .../board/repository/BoardRepositoryTest.java | 482 +++++++++--------- .../repository/CategoryRepositoryTest.java | 238 ++++----- .../repository/ChatRoomRepositoryTest.java | 8 +- .../repository/ChatRoomRepositoryTests.java | 12 +- 11 files changed, 406 insertions(+), 392 deletions(-) diff --git a/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java b/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java index a7c9e3c8..039d1d5d 100644 --- a/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java +++ b/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java @@ -32,7 +32,6 @@ public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; private final MemberRepository memberRepository; private final ParticipatedRoomRepository participatedRoomRepository; - private final IsReadRepository isReadRepository; private final ChatMessageRepository chatMessageRepository; // 채팅방 생성 @@ -91,10 +90,10 @@ public List get(Long memberId) { .participatedRoomId(room.getParticipatedRoomId()) .chatRoomId(room.getChatRoom().getChatRoomId()) .nickname(room.getParticipant().getNickname().getValue()) - .message(messages.get(0).getMessage()) + .message(messages.isEmpty()? null : messages.get(0).getMessage()) .chatRoomName(room.getChatRoom().getOccupiedRooms().get(1).getParticipant().getNickname().getValue()) - .lastChatTime(messages.get(0).getCreatedAt()) - .notReadCnt(unreadMessages.size()) + .lastChatTime(messages.isEmpty()? null : messages.get(0).getCreatedAt()) + .notReadCnt(unreadMessages.isEmpty()? null : unreadMessages.size()) .build(); log.info("room정보1 :{}", room.getParticipatedRoomId()); log.info("room정보2 :{}", room.getChatRoom().getChatRoomId()); diff --git a/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java b/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java index c6d14cbe..83fcd0d0 100644 --- a/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java +++ b/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; @Log4j2 @@ -68,7 +69,7 @@ public ResponseEntity create(@Validated @RequestBody MemberCreateRequest } @GetMapping("") - @PreAuthorize("hasRole('USER')") + @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @Operation(summary = "회원조회", description = "회원조회 요청 API") public ResponseEntity get(Authentication authentication) { String userId = authentication.getName(); @@ -76,7 +77,7 @@ public ResponseEntity get(Authentication authentication) { } @PutMapping("/{memberId}") - @PreAuthorize("hasRole('USER')") + @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @Operation(summary = "회원수정", description = "회원수정 요청 API") public ResponseEntity update(@Validated @RequestBody MemberUpdateRequest memberUpdateRequest, @PathVariable("memberId") Long memberId) { @@ -85,7 +86,7 @@ public ResponseEntity update(@Validated @RequestBody MemberUpdateRequest memb } @DeleteMapping("/{memberId}") - @PreAuthorize("hasRole('USER')") + @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @Operation(summary = "회원탈퇴", description = "회원탈퇴 요청 API") public ResponseEntity delete(@PathVariable("memberId") Long memberId) { memberUserService.delete(memberId); @@ -93,7 +94,7 @@ public ResponseEntity delete(@PathVariable("memberId") Long memberId) { } @GetMapping("/list") - @PreAuthorize("hasRole('USER')") + @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @Operation(summary = "회원리스트 조회", description = "회원리스트 조회 요청 API") public ResponseEntity> list(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "10") int size) { @@ -114,7 +115,7 @@ public ResponseEntity findPassword(@Validated @RequestBody MemberFindPas return ResponseEntity.ok(memberUserService.mailSend(mail)); } - @PreAuthorize("hasRole('USER')") + @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @PostMapping("/check-password") @Operation(summary = "비밀번호 확인", description = "비밀번호 확인 요청 API") public ResponseEntity checkPassword(@Validated @RequestBody PasswordDTO passwordDTO @@ -167,6 +168,19 @@ public ResponseEntity login(@Validated @RequestBody LoginRequest loginRequest //리프레시 토큰 레디스에 저장하기 refreshTokenService.insertInRedis(refreshPayloadMap, refreshToken); + + //로그인 시간 저장 + Member foundMember = memberRepository.findByUserId(new UserId(userId)) + .orElseThrow(()->new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); + foundMember.recordLoginTime(); + + //차단된 계정이면 차단 + if (!foundMember.getIsActive()){ + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("사용자는 "+foundMember.getReasonForBan() + +"문제로 인해 차단되었습니다."); + } + memberRepository.save(foundMember); + // 응답 반환 return ResponseEntity.ok(Map.of( "AccessToken", accessToken, diff --git a/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java b/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java index 5e91ee53..acf4377a 100644 --- a/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java @@ -60,24 +60,24 @@ public interface MemberRepository extends JpaRepository { //모든 회원조회 - UserId @Query("SELECT new com.mallangs.domain.member.dto.MemberGetResponseOnlyMember(m) " + "FROM Member m " + - "WHERE (:isActive IS NOT NULL OR m.isActive = :isActive) " + - "AND (:userId IS NOT NULL OR m.userId.value LIKE CONCAT('%', :userId, '%')) " + - "AND (:createAt IS NOT NULL OR m.createdAt >= :createAt)") + "WHERE (:isActive IS NULL OR m.isActive = :isActive) " + + "AND (:createAt IS NULL OR m.createdAt >= :createAt) " + + "AND m.userId = :userId") Page memberListByUserId( @Param("isActive") Boolean isActive, - @Param("userId") String userId, + @Param("userId") UserId userId, @Param("createAt") LocalDateTime createAt, Pageable pageable); //모든 회원조회 - Email @Query("SELECT new com.mallangs.domain.member.dto.MemberGetResponseOnlyMember(m) " + "FROM Member m " + - "WHERE (:isActive IS NOT NULL OR m.isActive = :isActive) " + - "AND (:email IS NOT NULL OR m.email.value LIKE CONCAT('%', :email, '%')) " + - "AND (:createAt IS NOT NULL OR m.createdAt >= :createAt)") + "WHERE (:isActive IS NULL OR m.isActive = :isActive) " + + "AND (:createAt IS NULL OR m.createdAt >= :createAt)" + + "AND m.email = :email") Page memberListByEmail( @Param("isActive") Boolean isActive, - @Param("email") String email, + @Param("email") Email email, @Param("createAt") LocalDateTime createAt, Pageable pageable); diff --git a/src/main/java/com/mallangs/domain/member/service/MemberAdminService.java b/src/main/java/com/mallangs/domain/member/service/MemberAdminService.java index d9a5ce5d..f6c799bb 100644 --- a/src/main/java/com/mallangs/domain/member/service/MemberAdminService.java +++ b/src/main/java/com/mallangs/domain/member/service/MemberAdminService.java @@ -1,6 +1,8 @@ package com.mallangs.domain.member.service; import com.mallangs.domain.member.dto.*; +import com.mallangs.domain.member.entity.embadded.Email; +import com.mallangs.domain.member.entity.embadded.UserId; import com.mallangs.domain.member.repository.MemberRepository; import com.mallangs.global.exception.ErrorCode; import com.mallangs.global.exception.MallangsCustomException; @@ -56,7 +58,7 @@ public Page getMemberListByUserId( Boolean isActive = memberGetRequestByUserId.getIsActive(); String userId = memberGetRequestByUserId.getUserId(); - return memberRepository.memberListByUserId(isActive, userId, createAt, pageable); + return memberRepository.memberListByUserId(isActive, new UserId(userId), createAt, pageable); } catch (Exception e) { log.error("회원리스트 조회에 실패하였습니다. {}", e.getMessage()); throw new MallangsCustomException(ErrorCode.FAILURE_REQUEST); @@ -75,7 +77,7 @@ public Page getMemberListByEmail( Boolean isActive = memberGetRequestByEmail.getIsActive(); String email = memberGetRequestByEmail.getEmail(); - return memberRepository.memberListByEmail(isActive, email, createAt, pageable); + return memberRepository.memberListByEmail(isActive, new Email(email), createAt, pageable); } catch (Exception e) { log.error("회원리스트 조회에 실패하였습니다. {}", e.getMessage()); throw new MallangsCustomException(ErrorCode.FAILURE_REQUEST); diff --git a/src/main/java/com/mallangs/global/config/MvcConfig.java b/src/main/java/com/mallangs/global/config/MvcConfig.java index d0e33a62..dc4b7df0 100644 --- a/src/main/java/com/mallangs/global/config/MvcConfig.java +++ b/src/main/java/com/mallangs/global/config/MvcConfig.java @@ -9,6 +9,7 @@ public class MvcConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 모든 경로에 대해 CORS 허용 .allowedOrigins("http://localhost:8080") + .allowedOrigins("http://localhost:5173") .allowedOrigins("http://localhost:3000", "https://*.ngrok-free.app") // 허용할 도메인 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 .allowedHeaders("*") // 허용할 헤더 diff --git a/src/main/java/com/mallangs/global/config/SecurityConfig.java b/src/main/java/com/mallangs/global/config/SecurityConfig.java index f6bdfd57..f0bf9ff5 100644 --- a/src/main/java/com/mallangs/global/config/SecurityConfig.java +++ b/src/main/java/com/mallangs/global/config/SecurityConfig.java @@ -85,7 +85,7 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws // oauth2 http .oauth2Login((oauth2) -> oauth2 - .loginPage("/login") + .loginPage("/login").permitAll() .userInfoEndpoint((userInfo) -> userInfo .userService(customOAuth2MemberService)) .successHandler(customSuccessHandler) diff --git a/src/test/java/com/mallangs/domain/article/repository/ArticleRepositoryTest.java b/src/test/java/com/mallangs/domain/article/repository/ArticleRepositoryTest.java index 9edbf460..1e730c63 100644 --- a/src/test/java/com/mallangs/domain/article/repository/ArticleRepositoryTest.java +++ b/src/test/java/com/mallangs/domain/article/repository/ArticleRepositoryTest.java @@ -6,6 +6,7 @@ import com.mallangs.domain.article.entity.LostArticle; import com.mallangs.domain.article.entity.LostStatus; import com.mallangs.domain.article.entity.MapVisibility; +import com.mallangs.domain.board.entity.BoardStatus; import com.mallangs.domain.member.entity.Member; import com.mallangs.domain.member.entity.embadded.Email; import com.mallangs.domain.member.entity.embadded.Nickname; @@ -68,6 +69,7 @@ void saveAndFindLostArticle() { .lastSeenLocation("공원") .lostStatus(LostStatus.LOST) .mapVisibility(MapVisibility.VISIBLE) + .articleStatus(BoardStatus.DRAFT) .type("lost") .title("강아지 찾아주세요") .geography(changeToPoint(-73.968285, 40.785091)) diff --git a/src/test/java/com/mallangs/domain/board/repository/BoardRepositoryTest.java b/src/test/java/com/mallangs/domain/board/repository/BoardRepositoryTest.java index e4dd83c0..01015c96 100644 --- a/src/test/java/com/mallangs/domain/board/repository/BoardRepositoryTest.java +++ b/src/test/java/com/mallangs/domain/board/repository/BoardRepositoryTest.java @@ -1,241 +1,241 @@ -package com.mallangs.domain.board.repository; - -import com.mallangs.domain.board.entity.Board; -import com.mallangs.domain.board.entity.BoardStatus; -import com.mallangs.domain.board.entity.BoardType; -import com.mallangs.domain.board.entity.Category; -import com.mallangs.domain.member.entity.Address; -import com.mallangs.domain.member.entity.Member; -import com.mallangs.domain.member.entity.embadded.Email; -import com.mallangs.domain.member.entity.embadded.Nickname; -import com.mallangs.domain.member.entity.embadded.Password; -import com.mallangs.domain.member.entity.embadded.UserId; -import com.mallangs.domain.member.repository.AddressRepository; -import com.mallangs.domain.member.repository.MemberRepository; -import com.mallangs.domain.member.util.GeometryUtil; -import jakarta.transaction.Transactional; -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.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -@Transactional -class BoardRepositoryTest { - - @Autowired - private BoardRepository boardRepository; - - @Autowired - private CategoryRepository categoryRepository; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private AddressRepository addressRepository; - - private Member saveMember() { - Member member = Member.builder() - .userId(new UserId("TestUser1")) - .nickname(new Nickname("JohnDoe")) - .email(new Email("john@doe.com")) - .password(new Password("1Q2w3e4r!", passwordEncoder)) - .hasPet(false) - .build(); - memberRepository.save(member); - - Address address = Address.builder() - .member(member) - .addressName("testAddress") - .addressType("testAddressType") - .mainAddressNo("testmainAddressNo") - .point(GeometryUtil.createPoint(1, 2)) - .mountainYn("testMountainYn") - .region1depthName("testRegion1depthName") - .region2depthName("testRegion2depthName") - .region3depthHName("testRegion3depthHName") - .region3depthName("testRegion3depthName") - .mainBuildingNo("testMainBuildingNo") - .subBuildingNo("testSubBuildingNo") - .roadName("testRoadName") - .buildingName("testBuildingName") - .subAddressNo("testSubAddressNo") - .zoneNo("testZoneNo") - .build(); - addressRepository.save(address); - member.addAddress(address); - return memberRepository.save(member); - } - - private Category saveCategory(String name) { - Category category = Category.builder() - .name(name) - .description("테스트 카테고리 설명") - .categoryLevel(0) - .categoryOrder(1) - .build(); - return categoryRepository.save(category); - } - - private Board saveCommunity(Member member, Category category, String title, String content) { - Board board = Board.builder() - .member(member) - .category(category) - .title(title) - .content(content) - .latitude(new BigDecimal("37.5665")) // 서울시청 위도 - .longitude(new BigDecimal("126.9780")) // 서울시청 경도 - .address("서울특별시 중구 세종대로 110") - .sightedAt(LocalDateTime.now()) - .imageId(1L) - .boardType(BoardType.COMMUNITY) - .build(); - return boardRepository.save(board); - } - - private Member testMember; - private Category testCategory; - private Board testBoard; - - @BeforeEach - void setUp() { - testMember = saveMember(); - testCategory = saveCategory("일반게시판"); - testBoard = saveCommunity(testMember, testCategory, "테스트 제목", "테스트 내용"); - } - - @Test - @DisplayName("카테고리별 게시글 조회 - PUBLISH만") - void findByCategoryId() { - // given - // DRAFT 상태의 게시글 생성 - Board draftBoard = saveCommunity(testMember, testCategory, "임시저장 게시글", "임시저장 내용"); - draftBoard.changeStatus(BoardStatus.DRAFT); - boardRepository.save(draftBoard); - - // when - Page result = boardRepository.findByCategoryId(testCategory.getCategoryId(), BoardType.COMMUNITY, PageRequest.of(0, 10)); - - // then - assertThat(result.getContent()).hasSize(1); // PUBLISH 상태의 게시글만 조회 - assertThat(result.getContent().get(0).getTitle()).isEqualTo("테스트 제목"); - assertThat(result.getContent().get(0).getBoardStatus()).isEqualTo(BoardStatus.PUBLISHED); - } - - @Test - @DisplayName("키워드로 게시글 검색 - PUBLISHED 상태만 검색되어야 함") - void searchByTitleOrContent() { - // given - Board draftBoard = saveCommunity(testMember, testCategory, "임시저장 테스트", "임시저장 내용"); - draftBoard.changeStatus(BoardStatus.DRAFT); - boardRepository.save(draftBoard); - - // when - Page result = boardRepository.searchByTitleOrContent("테스트", BoardType.COMMUNITY,PageRequest.of(0, 10)); - - // then - assertThat(result.getContent()).hasSize(1); // PUBLISHED 상태의 게시글만 조회 - Board foundBoard = result.getContent().get(0); - assertThat(foundBoard.getTitle()).contains("테스트"); - assertThat(foundBoard.getBoardStatus()).isEqualTo(BoardStatus.PUBLISHED); - } - - @Test - @DisplayName("회원별 게시글 조회 - PUBLISHED 상태만 조회되어야 함") - void findByMemberId() { - // given - Board hiddenBoard = saveCommunity(testMember, testCategory, "숨김 게시글", "숨김 내용"); - hiddenBoard.changeStatus(BoardStatus.HIDDEN); - boardRepository.save(hiddenBoard); - - // when - Page result = boardRepository.findByMemberId( - testMember.getMemberId(), - BoardType.COMMUNITY, - PageRequest.of(0, 10) - ); - - // then - assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getBoardStatus()).isEqualTo(BoardStatus.PUBLISHED); - } - - @Test - @DisplayName("상태별 게시글 조회") - void findByStatus() { - // given - Board draftBoard = saveCommunity(testMember, testCategory, "임시저장", "임시저장"); - draftBoard.changeStatus(BoardStatus.DRAFT); - boardRepository.save(draftBoard); - - // when - Page publishedResult = boardRepository.findByStatus( - BoardStatus.PUBLISHED, - PageRequest.of(0, 10) - ); - Page draftResult = boardRepository.findByStatus( - BoardStatus.DRAFT, - PageRequest.of(0, 10) - ); - - // then - assertThat(publishedResult.getContent()).hasSize(1); - assertThat(draftResult.getContent()).hasSize(1); - } - - @Test - @DisplayName("관리자용 - 카테고리와 제목으로 게시글 검색") - void searchForAdmin() { - // given - Board hiddenBoard = saveCommunity(testMember, testCategory, "테스트 숨김", "숨김 내용"); - hiddenBoard.changeStatus(BoardStatus.HIDDEN); - boardRepository.save(hiddenBoard); - - // when - Page result = boardRepository.searchForAdmin( - testCategory.getCategoryId(), - "테스트", - PageRequest.of(0, 10) - ); - - // then - assertThat(result.getContent()).hasSize(2); // 모든 상태의 게시글이 검색됨 - assertThat(result.getContent()).extracting("title") - .containsExactlyInAnyOrder("테스트 제목", "테스트 숨김"); - } - - @Test - @DisplayName("관리자용 - 카테고리, 상태, 제목으로 게시글 검색") - void searchForAdminWithStatus() { - // given - Board hiddenBoard = saveCommunity(testMember, testCategory, "테스트 숨김", "숨김 내용"); - hiddenBoard.changeStatus(BoardStatus.HIDDEN); - boardRepository.save(hiddenBoard); - - // when - Page result = boardRepository.searchForAdminWithStatus( - testCategory.getCategoryId(), - BoardStatus.HIDDEN, - "테스트", - PageRequest.of(0, 10) - ); - - // then - assertThat(result.getContent()).hasSize(1); - assertThat(result.getContent().get(0).getTitle()).isEqualTo("테스트 숨김"); - assertThat(result.getContent().get(0).getBoardStatus()).isEqualTo(BoardStatus.HIDDEN); - } -} \ No newline at end of file +//package com.mallangs.domain.board.repository; +// +//import com.mallangs.domain.board.entity.Board; +//import com.mallangs.domain.board.entity.BoardStatus; +//import com.mallangs.domain.board.entity.BoardType; +//import com.mallangs.domain.board.entity.Category; +//import com.mallangs.domain.member.entity.Address; +//import com.mallangs.domain.member.entity.Member; +//import com.mallangs.domain.member.entity.embadded.Email; +//import com.mallangs.domain.member.entity.embadded.Nickname; +//import com.mallangs.domain.member.entity.embadded.Password; +//import com.mallangs.domain.member.entity.embadded.UserId; +//import com.mallangs.domain.member.repository.AddressRepository; +//import com.mallangs.domain.member.repository.MemberRepository; +//import com.mallangs.domain.member.util.GeometryUtil; +//import jakarta.transaction.Transactional; +//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.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.security.crypto.password.PasswordEncoder; +// +//import java.math.BigDecimal; +//import java.time.LocalDateTime; +// +//import static org.assertj.core.api.Assertions.assertThat; +// +//@SpringBootTest +//@Transactional +//class BoardRepositoryTest { +// +// @Autowired +// private BoardRepository boardRepository; +// +// @Autowired +// private CategoryRepository categoryRepository; +// +// @Autowired +// private MemberRepository memberRepository; +// +// @Autowired +// private PasswordEncoder passwordEncoder; +// +// @Autowired +// private AddressRepository addressRepository; +// +// private Member saveMember() { +// Member member = Member.builder() +// .userId(new UserId("TestUser1")) +// .nickname(new Nickname("JohnDoe")) +// .email(new Email("john@doe.com")) +// .password(new Password("1Q2w3e4r!", passwordEncoder)) +// .hasPet(false) +// .build(); +// memberRepository.save(member); +// +// Address address = Address.builder() +// .member(member) +// .addressName("testAddress") +// .addressType("testAddressType") +// .mainAddressNo("testmainAddressNo") +// .point(GeometryUtil.createPoint(1, 2)) +// .mountainYn("testMountainYn") +// .region1depthName("testRegion1depthName") +// .region2depthName("testRegion2depthName") +// .region3depthHName("testRegion3depthHName") +// .region3depthName("testRegion3depthName") +// .mainBuildingNo("testMainBuildingNo") +// .subBuildingNo("testSubBuildingNo") +// .roadName("testRoadName") +// .buildingName("testBuildingName") +// .subAddressNo("testSubAddressNo") +// .zoneNo("testZoneNo") +// .build(); +// addressRepository.save(address); +// member.addAddress(address); +// return memberRepository.save(member); +// } +// +// private Category saveCategory(String name) { +// Category category = Category.builder() +// .name(name) +// .description("테스트 카테고리 설명") +// .categoryLevel(0) +// .categoryOrder(1) +// .build(); +// return categoryRepository.save(category); +// } +// +// private Board saveCommunity(Member member, Category category, String title, String content) { +// Board board = Board.builder() +// .member(member) +// .category(category) +// .title(title) +// .content(content) +// .latitude(new BigDecimal("37.5665")) // 서울시청 위도 +// .longitude(new BigDecimal("126.9780")) // 서울시청 경도 +// .address("서울특별시 중구 세종대로 110") +// .sightedAt(LocalDateTime.now()) +// .imageId(1L) +// .boardType(BoardType.COMMUNITY) +// .build(); +// return boardRepository.save(board); +// } +// +// private Member testMember; +// private Category testCategory; +// private Board testBoard; +// +// @BeforeEach +// void setUp() { +// testMember = saveMember(); +// testCategory = saveCategory("일반게시판"); +// testBoard = saveCommunity(testMember, testCategory, "테스트 제목", "테스트 내용"); +// } +// +// @Test +// @DisplayName("카테고리별 게시글 조회 - PUBLISH만") +// void findByCategoryId() { +// // given +// // DRAFT 상태의 게시글 생성 +// Board draftBoard = saveCommunity(testMember, testCategory, "임시저장 게시글", "임시저장 내용"); +// draftBoard.changeStatus(BoardStatus.DRAFT); +// boardRepository.save(draftBoard); +// +// // when +// Page result = boardRepository.findByCategoryId(testCategory.getCategoryId(), BoardType.COMMUNITY, PageRequest.of(0, 10)); +// +// // then +// assertThat(result.getContent()).hasSize(1); // PUBLISH 상태의 게시글만 조회 +// assertThat(result.getContent().get(0).getTitle()).isEqualTo("테스트 제목"); +// assertThat(result.getContent().get(0).getBoardStatus()).isEqualTo(BoardStatus.PUBLISHED); +// } +// +// @Test +// @DisplayName("키워드로 게시글 검색 - PUBLISHED 상태만 검색되어야 함") +// void searchByTitleOrContent() { +// // given +// Board draftBoard = saveCommunity(testMember, testCategory, "임시저장 테스트", "임시저장 내용"); +// draftBoard.changeStatus(BoardStatus.DRAFT); +// boardRepository.save(draftBoard); +// +// // when +// Page result = boardRepository.searchByTitleOrContent("테스트", BoardType.COMMUNITY,PageRequest.of(0, 10)); +// +// // then +// assertThat(result.getContent()).hasSize(1); // PUBLISHED 상태의 게시글만 조회 +// Board foundBoard = result.getContent().get(0); +// assertThat(foundBoard.getTitle()).contains("테스트"); +// assertThat(foundBoard.getBoardStatus()).isEqualTo(BoardStatus.PUBLISHED); +// } +// +// @Test +// @DisplayName("회원별 게시글 조회 - PUBLISHED 상태만 조회되어야 함") +// void findByMemberId() { +// // given +// Board hiddenBoard = saveCommunity(testMember, testCategory, "숨김 게시글", "숨김 내용"); +// hiddenBoard.changeStatus(BoardStatus.HIDDEN); +// boardRepository.save(hiddenBoard); +// +// // when +// Page result = boardRepository.findByMemberId( +// testMember.getMemberId(), +// BoardType.COMMUNITY, +// PageRequest.of(0, 10) +// ); +// +// // then +// assertThat(result.getContent()).hasSize(1); +// assertThat(result.getContent().get(0).getBoardStatus()).isEqualTo(BoardStatus.PUBLISHED); +// } +// +// @Test +// @DisplayName("상태별 게시글 조회") +// void findByStatus() { +// // given +// Board draftBoard = saveCommunity(testMember, testCategory, "임시저장", "임시저장"); +// draftBoard.changeStatus(BoardStatus.DRAFT); +// boardRepository.save(draftBoard); +// +// // when +// Page publishedResult = boardRepository.findByStatus( +// BoardStatus.PUBLISHED, +// PageRequest.of(0, 10) +// ); +// Page draftResult = boardRepository.findByStatus( +// BoardStatus.DRAFT, +// PageRequest.of(0, 10) +// ); +// +// // then +// assertThat(publishedResult.getContent()).hasSize(1); +// assertThat(draftResult.getContent()).hasSize(1); +// } +// +// @Test +// @DisplayName("관리자용 - 카테고리와 제목으로 게시글 검색") +// void searchForAdmin() { +// // given +// Board hiddenBoard = saveCommunity(testMember, testCategory, "테스트 숨김", "숨김 내용"); +// hiddenBoard.changeStatus(BoardStatus.HIDDEN); +// boardRepository.save(hiddenBoard); +// +// // when +// Page result = boardRepository.searchForAdmin( +// testCategory.getCategoryId(), +// "테스트", +// PageRequest.of(0, 10) +// ); +// +// // then +// assertThat(result.getContent()).hasSize(2); // 모든 상태의 게시글이 검색됨 +// assertThat(result.getContent()).extracting("title") +// .containsExactlyInAnyOrder("테스트 제목", "테스트 숨김"); +// } +// +// @Test +// @DisplayName("관리자용 - 카테고리, 상태, 제목으로 게시글 검색") +// void searchForAdminWithStatus() { +// // given +// Board hiddenBoard = saveCommunity(testMember, testCategory, "테스트 숨김", "숨김 내용"); +// hiddenBoard.changeStatus(BoardStatus.HIDDEN); +// boardRepository.save(hiddenBoard); +// +// // when +// Page result = boardRepository.searchForAdminWithStatus( +// testCategory.getCategoryId(), +// BoardStatus.HIDDEN, +// "테스트", +// PageRequest.of(0, 10) +// ); +// +// // then +// assertThat(result.getContent()).hasSize(1); +// assertThat(result.getContent().get(0).getTitle()).isEqualTo("테스트 숨김"); +// assertThat(result.getContent().get(0).getBoardStatus()).isEqualTo(BoardStatus.HIDDEN); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/mallangs/domain/board/repository/CategoryRepositoryTest.java b/src/test/java/com/mallangs/domain/board/repository/CategoryRepositoryTest.java index 52d90e5d..f760cc42 100644 --- a/src/test/java/com/mallangs/domain/board/repository/CategoryRepositoryTest.java +++ b/src/test/java/com/mallangs/domain/board/repository/CategoryRepositoryTest.java @@ -1,119 +1,119 @@ -package com.mallangs.domain.board.repository; - -import com.mallangs.domain.board.entity.Category; -import com.mallangs.domain.board.entity.CategoryStatus; -import jakarta.transaction.Transactional; -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 java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -@Transactional -class CategoryRepositoryTest { - - @Autowired - private CategoryRepository categoryRepository; - - private Category saveCategory(String name, int order, int level, CategoryStatus status) { - Category category = Category.builder() - .name(name) - .categoryOrder(order) - .categoryLevel(level) - .build(); - category.changeStatus(status); - return categoryRepository.save(category); - } - - @Test - @DisplayName("활성화된 카테고리 목록 조회") - void findAllActiveCategoriesTest() { - // given - saveCategory("일반게시판", 0, 0, CategoryStatus.ACTIVE); - saveCategory("실종게시판", 1, 1, CategoryStatus.ACTIVE); - saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); - - // when - List categories = categoryRepository.findAllActiveCategories(); - - // then - assertThat(categories).hasSize(2) - .extracting(Category::getName) - .containsExactly("일반게시판", "실종게시판"); - } - - @Test - @DisplayName("활성화된 카테고리 categoryId로 조회") - void findActiveCategoryByIdTest() { - // given - Category category = saveCategory("일반게시판", 52, 0, CategoryStatus.ACTIVE); - - // when - Optional foundCategory = categoryRepository.findActiveCategoryById(category.getCategoryId()); - - // then - assertThat(foundCategory).isPresent(); - assertThat(foundCategory.get().getName()).isEqualTo("일반게시판"); - } - - @Test - @DisplayName("상태별 카테고리 목록 조회") - void findAllByStatusOrderByCategoryOrder() { - // given - saveCategory("실종게시판", 1, 0, CategoryStatus.ACTIVE); - saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); - - // when - List activeCategories = categoryRepository.findAllByStatusOrderByCategoryOrder(CategoryStatus.ACTIVE); - List inactiveCategories = categoryRepository.findAllByStatusOrderByCategoryOrder(CategoryStatus.INACTIVE); - - // then - assertThat(activeCategories).hasSize(1) - .extracting(Category::getName) - .containsExactly("실종게시판"); - - assertThat(inactiveCategories).hasSize(1) - .extracting(Category::getName) - .containsExactly("목격게시판"); - } - - @Test - @DisplayName("이름으로 카테고리 검색") - void findByNameContaining() { - // given - saveCategory("일반게시판", 0, 0, CategoryStatus.ACTIVE); - saveCategory("실종게시판", 1, 0, CategoryStatus.INACTIVE); - saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); - saveCategory("정보게시판", 3, 1, CategoryStatus.INACTIVE); - - // when - List categories = categoryRepository.findByNameContaining("일반"); - - // then - assertThat(categories).hasSize(1) - .extracting(Category::getName) - .containsExactly("일반게시판"); - } - - @Test - @DisplayName("카테고리 상태별 개수 확인") - void countByCategoryStatus() { - // given - saveCategory("일반게시판", 0, 0, CategoryStatus.ACTIVE); - saveCategory("실종게시판", 1, 0, CategoryStatus.INACTIVE); - saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); - - // when - long activeCount = categoryRepository.countByCategoryStatus(CategoryStatus.ACTIVE); - long inactiveCount = categoryRepository.countByCategoryStatus(CategoryStatus.INACTIVE); - - // then - assertThat(activeCount).isEqualTo(1); - assertThat(inactiveCount).isEqualTo(2); - } -} \ No newline at end of file +//package com.mallangs.domain.board.repository; +// +//import com.mallangs.domain.board.entity.Category; +//import com.mallangs.domain.board.entity.CategoryStatus; +//import jakarta.transaction.Transactional; +//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 java.util.List; +//import java.util.Optional; +// +//import static org.assertj.core.api.Assertions.assertThat; +// +//@SpringBootTest +//@Transactional +//class CategoryRepositoryTest { +// +// @Autowired +// private CategoryRepository categoryRepository; +// +// private Category saveCategory(String name, int order, int level, CategoryStatus status) { +// Category category = Category.builder() +// .name(name) +// .categoryOrder(order) +// .categoryLevel(level) +// .build(); +// category.changeStatus(status); +// return categoryRepository.save(category); +// } +// +// @Test +// @DisplayName("활성화된 카테고리 목록 조회") +// void findAllActiveCategoriesTest() { +// // given +// saveCategory("일반게시판", 0, 0, CategoryStatus.ACTIVE); +// saveCategory("실종게시판", 1, 1, CategoryStatus.ACTIVE); +// saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); +// +// // when +// List categories = categoryRepository.findAllActiveCategories(); +// +// // then +// assertThat(categories).hasSize(2) +// .extracting(Category::getName) +// .containsExactly("일반게시판", "실종게시판"); +// } +// +// @Test +// @DisplayName("활성화된 카테고리 categoryId로 조회") +// void findActiveCategoryByIdTest() { +// // given +// Category category = saveCategory("일반게시판", 52, 0, CategoryStatus.ACTIVE); +// +// // when +// Optional foundCategory = categoryRepository.findActiveCategoryById(category.getCategoryId()); +// +// // then +// assertThat(foundCategory).isPresent(); +// assertThat(foundCategory.get().getName()).isEqualTo("일반게시판"); +// } +// +// @Test +// @DisplayName("상태별 카테고리 목록 조회") +// void findAllByStatusOrderByCategoryOrder() { +// // given +// saveCategory("실종게시판", 1, 0, CategoryStatus.ACTIVE); +// saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); +// +// // when +// List activeCategories = categoryRepository.findAllByStatusOrderByCategoryOrder(CategoryStatus.ACTIVE); +// List inactiveCategories = categoryRepository.findAllByStatusOrderByCategoryOrder(CategoryStatus.INACTIVE); +// +// // then +// assertThat(activeCategories).hasSize(1) +// .extracting(Category::getName) +// .containsExactly("실종게시판"); +// +// assertThat(inactiveCategories).hasSize(1) +// .extracting(Category::getName) +// .containsExactly("목격게시판"); +// } +// +// @Test +// @DisplayName("이름으로 카테고리 검색") +// void findByNameContaining() { +// // given +// saveCategory("일반게시판", 0, 0, CategoryStatus.ACTIVE); +// saveCategory("실종게시판", 1, 0, CategoryStatus.INACTIVE); +// saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); +// saveCategory("정보게시판", 3, 1, CategoryStatus.INACTIVE); +// +// // when +// List categories = categoryRepository.findByNameContaining("일반"); +// +// // then +// assertThat(categories).hasSize(1) +// .extracting(Category::getName) +// .containsExactly("일반게시판"); +// } +// +// @Test +// @DisplayName("카테고리 상태별 개수 확인") +// void countByCategoryStatus() { +// // given +// saveCategory("일반게시판", 0, 0, CategoryStatus.ACTIVE); +// saveCategory("실종게시판", 1, 0, CategoryStatus.INACTIVE); +// saveCategory("목격게시판", 2, 1, CategoryStatus.INACTIVE); +// +// // when +// long activeCount = categoryRepository.countByCategoryStatus(CategoryStatus.ACTIVE); +// long inactiveCount = categoryRepository.countByCategoryStatus(CategoryStatus.INACTIVE); +// +// // then +// assertThat(activeCount).isEqualTo(1); +// assertThat(inactiveCount).isEqualTo(2); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTest.java b/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTest.java index 2cc1880f..d5400145 100644 --- a/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTest.java +++ b/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTest.java @@ -18,9 +18,6 @@ @SpringBootTest public class ChatRoomRepositoryTest { -<<<<<<< HEAD -} -======= @Autowired private PasswordEncoder passwordEncoder; @@ -98,11 +95,11 @@ public void test() { IsRead isRead = IsRead.builder() .chatMessage(chatMessage) - .sender(member.getNickname().getValue()).build(); + .reader(member.getNickname().getValue()).build(); isReadRepository.save(isRead); IsRead isRead2 = IsRead.builder() .chatMessage(chatMessage2) - .sender(member2.getNickname().getValue()).build(); + .reader(member2.getNickname().getValue()).build(); isReadRepository.save(isRead2); //when @@ -115,4 +112,3 @@ public void test() { } } ->>>>>>> 3579dd746bc2bb818ef6786e0e9330f2c28f3f2c diff --git a/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTests.java b/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTests.java index 9369998b..7b8188ae 100644 --- a/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTests.java +++ b/src/test/java/com/mallangs/domain/chat/repository/ChatRoomRepositoryTests.java @@ -52,17 +52,17 @@ public class ChatRoomRepositoryTests { public void 모든데이터값넣기() { // Member 저장 Member member = memberRepository.save(Member.builder() - .userId(new UserId("TestUserId")) + .userId(new UserId("TtUser11")) .nickname(new Nickname("TestNickname")) - .email(new Email("Test@eamil.com")) + .email(new Email("Tasdt@eami1s1l.com")) .password(new Password("1234aA!!", passwordEncoder)) .hasPet(true) .build()); Member member2 = memberRepository.save(Member.builder() - .userId(new UserId("TestAdminId")) + .userId(new UserId("Te1dmnI1d")) .nickname(new Nickname("TestNickname2")) - .email(new Email("Test2@eamil.com")) + .email(new Email("Tes2@eami1l.com")) .password(new Password("1234aA!!", passwordEncoder)) .memberRole(MemberRole.ROLE_ADMIN) .hasPet(false) @@ -87,12 +87,12 @@ public class ChatRoomRepositoryTests { // Image 저장 Image savedImage1 = imageRepository.save(Image.builder() - .url("qwerqweasd") + .url("qwerqaasdsd") .width(1234) .height(1234) .build()); Image savedImage2 = imageRepository.save(Image.builder() - .url("qwerqwea1sd") + .url("qwerea1asdsd") .width(12345) .height(12354) .build()); From 7a8df54478c1219ff9994e64e81204e3637c8d31 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Tue, 3 Dec 2024 14:11:21 +0900 Subject: [PATCH 02/12] a --- .../controller/MemberUserController.java | 37 ------------------- .../member/repository/MemberRepository.java | 13 ------- 2 files changed, 50 deletions(-) diff --git a/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java b/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java index 84822d96..6032e35f 100644 --- a/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java +++ b/src/main/java/com/mallangs/domain/member/controller/MemberUserController.java @@ -75,28 +75,19 @@ public ResponseEntity create(@Validated @RequestBody MemberCreateRequest } @GetMapping("") -<<<<<<< HEAD - @PreAuthorize("hasAnyRole('USER', 'ADMIN')") - @Operation(summary = "회원조회", description = "회원조회 요청 API") -======= @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") @Operation(summary = "회원 프로필 조회", description = "회원 프로필 조회 요청 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "회원 조회 성공"), @ApiResponse(responseCode = "404", description = "회원조회에 실패하였습니다.") }) ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 public ResponseEntity get(Authentication authentication) { String userId = authentication.getName(); return ResponseEntity.ok(memberUserService.get(userId)); } @PutMapping("/{memberId}") -<<<<<<< HEAD - @PreAuthorize("hasAnyRole('USER', 'ADMIN')") -======= @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 @Operation(summary = "회원수정", description = "회원수정 요청 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "회원 수정 성공"), @@ -108,21 +99,8 @@ public ResponseEntity update(@Validated @RequestBody MemberUpdateRequest memb return ResponseEntity.ok().build(); } -<<<<<<< HEAD - @DeleteMapping("/{memberId}") - @PreAuthorize("hasAnyRole('USER', 'ADMIN')") - @Operation(summary = "회원탈퇴", description = "회원탈퇴 요청 API") - public ResponseEntity delete(@PathVariable("memberId") Long memberId) { - memberUserService.delete(memberId); - return ResponseEntity.ok().build(); - } - - @GetMapping("/list") - @PreAuthorize("hasAnyRole('USER', 'ADMIN')") -======= @GetMapping("/list") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 @Operation(summary = "회원리스트 조회", description = "회원리스트 조회 요청 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "회원 리스트 조회 성공"), @@ -155,11 +133,7 @@ public ResponseEntity findPassword(@Validated @RequestBody MemberFindPas return ResponseEntity.ok(memberUserService.mailSend(mail)); } -<<<<<<< HEAD - @PreAuthorize("hasAnyRole('USER', 'ADMIN')") -======= @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 @PostMapping("/check-password") @Operation(summary = "비밀번호 확인", description = "비밀번호 확인 요청 API") @ApiResponses({ @@ -221,16 +195,6 @@ public ResponseEntity login(@Validated @RequestBody LoginRequest loginRequest Member foundMember = memberRepository.findByUserId(new UserId(userId)) .orElseThrow(()->new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); foundMember.recordLoginTime(); -<<<<<<< HEAD - - //차단된 계정이면 차단 - if (!foundMember.getIsActive()){ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("사용자는 "+foundMember.getReasonForBan() - +"문제로 인해 차단되었습니다."); - } - memberRepository.save(foundMember); - -======= memberRepository.save(foundMember); //차단계정인지 확인 @@ -240,7 +204,6 @@ public ResponseEntity login(@Validated @RequestBody LoginRequest loginRequest + foundMember.getExpiryDate() + " 까지 웹서비스 이용 제한됩니다."); } ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 // 응답 반환 return ResponseEntity.ok(Map.of( "AccessToken", accessToken, diff --git a/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java b/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java index d1d23da4..f27b8137 100644 --- a/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/mallangs/domain/member/repository/MemberRepository.java @@ -68,13 +68,8 @@ public interface MemberRepository extends JpaRepository { @Query("SELECT new com.mallangs.domain.member.dto.MemberGetResponseOnlyMember(m) " + "FROM Member m " + "WHERE (:isActive IS NULL OR m.isActive = :isActive) " + -<<<<<<< HEAD - "AND (:createAt IS NULL OR m.createdAt >= :createAt) " + - "AND m.userId = :userId") -======= "AND (:userId IS NULL OR m.userId.value = :userId) " + "AND (:createAt IS NULL OR m.createdAt >= :createAt)") ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 Page memberListByUserId( @Param("isActive") Boolean isActive, @Param("userId") UserId userId, @@ -82,19 +77,11 @@ Page memberListByUserId( Pageable pageable); //모든 회원조회 - Email -<<<<<<< HEAD - @Query("SELECT new com.mallangs.domain.member.dto.MemberGetResponseOnlyMember(m) " + - "FROM Member m " + - "WHERE (:isActive IS NULL OR m.isActive = :isActive) " + - "AND (:createAt IS NULL OR m.createdAt >= :createAt)" + - "AND m.email = :email") -======= @Query(" SELECT new com.mallangs.domain.member.dto.MemberGetResponseOnlyMember(m) " + " FROM Member m " + " WHERE (:isActive IS NULL OR m.isActive = :isActive) " + " AND (:email IS NULL OR m.email.value = :email)" + " AND (:createAt IS NULL OR m.createdAt >= :createAt) ") ->>>>>>> 73aa0cbc01cf0525b48bcfbb74d455eb9df33cf8 Page memberListByEmail( @Param("isActive") Boolean isActive, @Param("email") Email email, From a6e40f658806475b5ec537d886afebc39b6f9d07 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Tue, 24 Dec 2024 14:25:46 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[feat]=20AI,=EB=AA=A9=EA=B2=A9=20?= =?UTF-8?q?=EA=B8=80=ED=83=80=EB=9E=98=20=EC=B6=94=EA=B0=80=20(AI=20?= =?UTF-8?q?=EB=AF=B8=EC=99=84,=20=EB=AA=A9=EA=B2=A9=EA=B8=80=ED=83=80?= =?UTF-8?q?=EB=9E=98=20=EB=AF=B8=EC=99=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +- .../domain/ai/AIPromptController.java | 109 ++++ .../mallangs/domain/ai/dto/AiResponseDTO.java | 15 + .../article/controller/ArticleController.java | 418 +++++++------ .../request/SightingArticleCreateRequest.java | 43 ++ .../dto/response/SightingArticleResponse.java | 50 ++ .../domain/article/entity/ArticleType.java | 5 +- .../article/entity/SightingArticle.java | 108 ++++ .../article/factory/SightArticleFactory.java | 30 + .../article/repository/ArticleRepository.java | 5 + .../article/service/ArticleService.java | 592 ++++++++++-------- .../board/controller/BoardController.java | 2 + .../board/repository/BoardRepository.java | 5 + .../domain/board/service/BoardService.java | 6 + .../chat/controller/ChatRoomController.java | 7 +- .../dto/response/ChatMessageListResponse.java | 5 - .../chat/dto/response/ChatRoomResponse.java | 6 +- .../chat/service/ChatMessageService.java | 4 +- .../domain/chat/service/ChatRoomService.java | 2 +- .../mallangs/global/exception/ErrorCode.java | 16 +- 20 files changed, 950 insertions(+), 486 deletions(-) create mode 100644 src/main/java/com/mallangs/domain/ai/AIPromptController.java create mode 100644 src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java create mode 100644 src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java create mode 100644 src/main/java/com/mallangs/domain/article/dto/response/SightingArticleResponse.java create mode 100644 src/main/java/com/mallangs/domain/article/entity/SightingArticle.java create mode 100644 src/main/java/com/mallangs/domain/article/factory/SightArticleFactory.java diff --git a/build.gradle b/build.gradle index d1d7b03b..41fe7479 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ configurations { repositories { mavenCentral() + maven {url 'https://repo.spring.io/snapshot'} //스냅샷 저장소 추가 } dependencies { @@ -70,7 +71,6 @@ dependencies { implementation 'org.webjars:stomp-websocket:2.3.3-1' // 지리 데이터 - implementation 'org.hibernate.orm:hibernate-spatial:6.5.3.Final' implementation 'com.querydsl:querydsl-spatial' //테스트 @@ -106,6 +106,8 @@ dependencies { implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.1.1' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.777' + //AI prompt + implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter:1.0.0-SNAPSHOT' } jar { @@ -114,3 +116,7 @@ jar { tasks.named('test') { useJUnitPlatform() } +//긴 클래스 패스 길이 해결 +tasks.withType(JavaExec).configureEach { + classpath = files(sourceSets.main.runtimeClasspath.files) +} diff --git a/src/main/java/com/mallangs/domain/ai/AIPromptController.java b/src/main/java/com/mallangs/domain/ai/AIPromptController.java new file mode 100644 index 00000000..9712db33 --- /dev/null +++ b/src/main/java/com/mallangs/domain/ai/AIPromptController.java @@ -0,0 +1,109 @@ +package com.mallangs.domain.ai; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mallangs.domain.ai.dto.AiResponseDTO; +import com.mallangs.domain.article.dto.response.ArticleResponse; +import com.mallangs.domain.article.service.ArticleService; +import com.mallangs.domain.board.dto.response.SightingListResponse; +import com.mallangs.domain.board.service.BoardService; +import com.mallangs.domain.member.entity.Member; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Log4j2 +@RestController +@RequestMapping("/api/v1/ai") +@RequiredArgsConstructor +public class AIPromptController { + private final VertexAiGeminiChatModel vertexAiGeminiChatModel; + private final ArticleService articleService; + private final BoardService boardService; + + @GetMapping("/chat") + public ResponseEntity chat() { + try { + //질문내용 + String message = "{ \"response\": \"너는 누구야?\", \"details\": \"json 형식으로 답변해줘\" }"; + + // 1. Gemini에게 질문 (질문 -> 응답) + String vertexAiGeminiResponse = vertexAiGeminiChatModel.call(message); + + // 응답에서 백틱 및 불필요한 텍스트 제거 + String cleanedResponse = cleanJsonResponse(vertexAiGeminiResponse); + + // json 형식인지 확인 + if (!cleanedResponse.trim().startsWith("{")) { + log.error("Invalid response format: {}", cleanedResponse); + throw new IllegalArgumentException("AI 응답이 JSON 형식이 아닙니다."); + } + + // 2. JSON 데이터를 DTO로 매핑 + ObjectMapper objectMapper = new ObjectMapper(); + AiResponseDTO responseDto = objectMapper.readValue(cleanedResponse, AiResponseDTO.class); + + return ResponseEntity.ok(responseDto); + } catch (IllegalArgumentException e) { + log.error("AI answer failed {}",e.getMessage()); + throw new RuntimeException("AI 응답이 유효하지 않습니다."); + } catch (Exception e) { + log.error("Error while processing AI response: {}", e.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답 처리 중 문제가 발생했습니다.", e); + } + } + /** + * AI 응답에서 백틱 및 불필요한 텍스트 제거 + */ + private String cleanJsonResponse(String response) { + // 백틱과 "```json" 제거 + String cleaned = response.replaceAll("```json", "").replaceAll("```", "").trim(); + log.debug("Cleaned response: {}", cleaned); + return cleaned; + } + + // AI로 비슷한 목격게시물 조회 + // 관리자 전부 조회 가능 + // 회원 visible + 자신의 글 조회 가능 + // 비회원 mapVisible 만 조회 가능 + @Operation(summary = "AI로 비슷한 목격게시물을 조회", description = "AI로 실종글타래와 비슷한 목격게시물을 조회합니다.") + @GetMapping("/{articleId}") + public ResponseEntity getArticleByArticleIdByAI( + @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { + + String memberRole; + Long memberId; + + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (principal instanceof Member member) { + memberRole = member.getMemberRole().name(); + memberId = member.getMemberId(); + } else { + memberRole = "ROLE_GUEST"; + memberId = -1L; + } + log.info("role: {} memberId: {}", memberRole, memberId); + + //단견조회, 실종글타래 조회 + ArticleResponse articleResponse = articleService.getArticleById(articleId, memberRole, + memberId); + + //목격제보 게시글 전체조회 + List sightingList = boardService.getAllSightingBoardToList(); + + return ResponseEntity.ok(articleResponse); + } + +} diff --git a/src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java b/src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java new file mode 100644 index 00000000..e8a564da --- /dev/null +++ b/src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java @@ -0,0 +1,15 @@ +package com.mallangs.domain.ai.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) // DTO에 정의되지 않은 필드는 무시 +public class AiResponseDTO { + @JsonProperty("response") + private String response; + + @JsonProperty("details") + private String details; +} diff --git a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java index 5c65255a..d9269902 100644 --- a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java +++ b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java @@ -9,13 +9,17 @@ import com.mallangs.domain.article.service.ArticleService; import com.mallangs.domain.article.service.LocationService; import com.mallangs.domain.article.validation.ValidationGroups; +import com.mallangs.domain.board.dto.response.SightingListResponse; +import com.mallangs.domain.board.service.BoardService; import com.mallangs.domain.member.entity.Member; import com.mallangs.global.jwt.entity.CustomMemberDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; + import java.util.List; import java.util.Objects; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -43,212 +47,224 @@ @Tag(name = "Article Controller", description = "글타래 API") public class ArticleController { - private final ArticleService articleService; - private final LocationService locationService; - - @Operation(summary = "글타래 등록", description = "새로운 글타래를 등록합니다.") - @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") - @PostMapping - public ResponseEntity createArticle( - @Validated(ValidationGroups.CreateGroup.class) @RequestBody ArticleCreateRequest articleCreateRequest, - @Parameter(description = "현재 인증된 사용자 정보", required = true) - @AuthenticationPrincipal CustomMemberDetails principal) { - Long memberId = principal.getMemberId(); - ArticleResponse articleResponse = articleService.createArticle(articleCreateRequest, - memberId); - - return ResponseEntity.ok(articleResponse); - } - - - // 조회 - // 관리자 전부 조회 가능 - // 회원 visible + 자신의 글 조회 가능 - // 비회원 mapVisible 만 조회 가능 - @Operation(summary = "글타래 단건 조회", description = "글타래를 단건 조회합니다.") - @GetMapping("/public/{articleId}") - public ResponseEntity getArticleByArticleId( - @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { - - String memberRole; - Long memberId; - - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - - if (principal instanceof Member member) { - memberRole = member.getMemberRole().name(); - memberId = member.getMemberId(); - } else { - memberRole = "ROLE_GUEST"; - memberId = -1L; + private final ArticleService articleService; + private final LocationService locationService; + private final BoardService boardService; + + @Operation(summary = "글타래 등록", description = "새로운 글타래를 등록합니다.") + @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") + @PostMapping + public ResponseEntity createArticle( + @Validated(ValidationGroups.CreateGroup.class) @RequestBody ArticleCreateRequest articleCreateRequest, + @Parameter(description = "현재 인증된 사용자 정보", required = true) + @AuthenticationPrincipal CustomMemberDetails principal) { + Long memberId = principal.getMemberId(); + ArticleResponse articleResponse = articleService.createArticle(articleCreateRequest, + memberId); + + return ResponseEntity.ok(articleResponse); + } + + // 조회 + // 관리자 전부 조회 가능 + // 회원 visible + 자신의 글 조회 가능 + // 비회원 mapVisible 만 조회 가능 + @Operation(summary = "글타래 단건 조회", description = "글타래를 단건 조회합니다.") + @GetMapping("/public/{articleId}") + public ResponseEntity getArticleByArticleId( + @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { + + String memberRole; + Long memberId; + + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + log.info("principal 단건조회 :{}", principal); + + if (principal instanceof Member member) { + memberRole = member.getMemberRole().name(); + memberId = member.getMemberId(); + } else { + memberRole = "ROLE_GUEST"; + memberId = -1L; + } + log.info("role: {} memberId: {}", memberRole, memberId); + + ArticleResponse articleResponse = articleService.getArticleById(articleId, memberRole, + memberId); + + return ResponseEntity.ok(articleResponse); + } + + // 관리자 페이지 + // articleType : lost rescue place user + // placeCategory : place 하위 + @Operation(summary = "관리자 글타래 전체 조회", description = "관리자가 글타래를 조회합니다.") + @PreAuthorize("hasAuthority('ROLE_ADMIN')") + @GetMapping("/admin") + public ResponseEntity getArticles( + @PageableDefault(page = 0, size = 10) + @Parameter(description = "페이징 요청 정보", required = true) Pageable pageable, + @RequestParam(value = "articleType", required = false) String articleType, // 대분류 + @RequestParam(value = "placeCategory", required = false) String placeCategory) { // 소분류 + + ArticlePageResponse articles; + + if (articleType == null || articleType.isEmpty()) { + articles = articleService.findAllTypeArticles(pageable); // 전체 + } else { + if (placeCategory == null || placeCategory.isEmpty()) { // 대분류 + articles = articleService.findArticlesByArticleType(pageable, articleType); + } else { // 장소, 사용자 등록 위치 소분류 + articles = articleService.findPlaceArticlesByCategory(pageable, articleType, placeCategory); + } + } + + return ResponseEntity.ok(articles); + } + + // 실종 페이지 // list + // map visible 만 보임 + @Operation(summary = "실종 글타래 전체 조회", description = "실종 글타래를 조회합니다.") + @GetMapping("/public/lost") + public ResponseEntity> getLostArticles( + @RequestParam(value = "lostStatus", required = false) CaseStatus lostStatus) { + + List articles = articleService.findLostArticles(lostStatus); + + return ResponseEntity.ok(articles); } - log.info("role: {} memberId: {}", memberRole, memberId); - - ArticleResponse articleResponse = articleService.getArticleById(articleId, memberRole, - memberId); - - return ResponseEntity.ok(articleResponse); - } - - // 관리자 페이지 - // articleType : lost rescue place user - // placeCategory : place 하위 - @Operation(summary = "관리자 글타래 전체 조회", description = "관리자가 글타래를 조회합니다.") - @PreAuthorize("hasAuthority('ROLE_ADMIN')") - @GetMapping("/admin") - public ResponseEntity getArticles( - @PageableDefault(page = 0, size = 10) - @Parameter(description = "페이징 요청 정보", required = true) Pageable pageable, - @RequestParam(value = "articleType", required = false) String articleType, // 대분류 - @RequestParam(value = "placeCategory", required = false) String placeCategory) { // 소분류 - - ArticlePageResponse articles; - - if (articleType == null || articleType.isEmpty()) { - articles = articleService.findAllTypeArticles(pageable); // 전체 - } else { - if (placeCategory == null || placeCategory.isEmpty()) { // 대분류 - articles = articleService.findArticlesByArticleType(pageable, articleType); - } else { // 장소, 사용자 등록 위치 소분류 - articles = articleService.findPlaceArticlesByCategory(pageable, articleType, placeCategory); - } + + + // 지도에 마커 표시 위한 경로 + // 위치 기준 지도 전체 글타래 조회 // 타입별 조회 + // map visible 만 보임 + @Operation(summary = "지도에서 글타래 조회", description = "지도에서 글타래를 조회합니다.") + @PostMapping("/public/articlesMarkers") + public ResponseEntity> getMarkersInBounds( + @RequestParam(value = "articleType", required = false) String articleType, + @RequestParam(value = "placeCategory", required = false) String placeCategory, + @RequestBody MapBoundsRequest bounds) { + + double southWestLat = bounds.getSouthWestLat(); + double southWestLon = bounds.getSouthWestLon(); + double northEastLat = bounds.getNorthEastLat(); + double northEastLon = bounds.getNorthEastLon(); + + List articlesInBounds; + + log.info("지도에서 글타래 조회"); + + // 대분류 소분류 + // 대분류 null 인경우 전체 조회 + // 대분류 존재하는 경우 해당 값 조회 // 시설/위치 ---구조 ---목격 ---사용자 등록 정보 + // 시설/위치는 소분류도 존재 + // 사용자 등록 정보는? + if (Objects.equals(articleType, "all") || articleType == null + || articleType.isEmpty()) { // 대분류 null 전체 조회 + log.info("전체 조회"); + articlesInBounds = locationService.findArticlesInBounds( + southWestLat, southWestLon, + northEastLat, northEastLon); + } else { // 대준류 null 아님 + log.info("대분류 조회"); + if (placeCategory == null || placeCategory.isEmpty()) { // 소분류 없는 경우 // 글타래 타입 기준 조회 + articlesInBounds = locationService.findArticlesInBoundsByType( + southWestLat, southWestLon, + northEastLat, northEastLon, articleType); // 실종 목격 구조 장소 사용자등록장소 + } else { // 시설 업체, 사용자 등록 => 장소 소분류 존재 + articlesInBounds = locationService.findPlaceArticlesInBoundsByCategory( + southWestLat, southWestLon, + northEastLat, northEastLon, articleType, placeCategory); + } + } + + return ResponseEntity.ok(articlesInBounds); } - return ResponseEntity.ok(articles); - } - - // 실종 페이지 // list - // map visible 만 보임 - @Operation(summary = "실종 글타래 전체 조회", description = "실종 글타래를 조회합니다.") - @GetMapping("/public/lost") - public ResponseEntity> getLostArticles( - @RequestParam(value = "lostStatus", required = false) CaseStatus lostStatus) { - - List articles = articleService.findLostArticles(lostStatus); - - return ResponseEntity.ok(articles); - } - - - // 지도에 마커 표시 위한 경로 - // 위치 기준 지도 전체 글타래 조회 // 타입별 조회 - // map visible 만 보임 - @Operation(summary = "지도에서 글타래 조회", description = "지도에서 글타래를 조회합니다.") - @PostMapping("/public/articlesMarkers") - public ResponseEntity> getMarkersInBounds( - @RequestParam(value = "articleType", required = false) String articleType, - @RequestParam(value = "placeCategory", required = false) String placeCategory, - @RequestBody MapBoundsRequest bounds) { - - double southWestLat = bounds.getSouthWestLat(); - double southWestLon = bounds.getSouthWestLon(); - double northEastLat = bounds.getNorthEastLat(); - double northEastLon = bounds.getNorthEastLon(); - - List articlesInBounds; - - log.info("지도에서 글타래 조회"); - - // 대분류 소분류 - // 대분류 null 인경우 전체 조회 - // 대분류 존재하는 경우 해당 값 조회 // 시설/위치 ---구조 ---목격 ---사용자 등록 정보 - // 시설/위치는 소분류도 존재 - // 사용자 등록 정보는? - if (Objects.equals(articleType, "all") || articleType == null - || articleType.isEmpty()) { // 대분류 null 전체 조회 - log.info("전체 조회"); - articlesInBounds = locationService.findArticlesInBounds( - southWestLat, southWestLon, - northEastLat, northEastLon); - } else { // 대준류 null 아님 - log.info("대분류 조회"); - if (placeCategory == null || placeCategory.isEmpty()) { // 소분류 없는 경우 // 글타래 타입 기준 조회 - articlesInBounds = locationService.findArticlesInBoundsByType( - southWestLat, southWestLon, - northEastLat, northEastLon, articleType); // 실종 목격 구조 장소 사용자등록장소 - } else { // 시설 업체, 사용자 등록 => 장소 소분류 존재 - articlesInBounds = locationService.findPlaceArticlesInBoundsByCategory( - southWestLat, southWestLon, - northEastLat, northEastLon, articleType, placeCategory); - } + // 검색 조회 + // map visible 만 보임 + @Operation(summary = "글타래 검색", description = "글타래에서 검색합니다.") + @GetMapping("/public/search") + public ResponseEntity> searchSightingPosts( + @RequestParam String keyword) { + + List articles = articleService.findArticlesByKeyword(keyword); + return ResponseEntity.ok(articles); } - return ResponseEntity.ok(articlesInBounds); - } - - // 검색 조회 - // map visible 만 보임 - @Operation(summary = "글타래 검색", description = "글타래에서 검색합니다.") - @GetMapping("/public/search") - public ResponseEntity> searchSightingPosts( - @RequestParam String keyword) { - - List articles = articleService.findArticlesByKeyword(keyword); - return ResponseEntity.ok(articles); - } - - - // 회원 자신이 작성한 글타래 목록 조회 // list - // is deleted false 안 보임 - @Operation(summary = "사용자가 작성한 전체 글타래 조회", description = "사용자가 자신이 작성한 글타래 목록을 조회합니다.") - @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") - @GetMapping("/myArticles") - public ResponseEntity> getArticlesByMemberId( - @Parameter(description = "현재 인증된 사용자 정보", required = true) - @AuthenticationPrincipal CustomMemberDetails principal) { - - List articles = articleService.findArticlesByMemberId(principal.getMemberId()); - return ResponseEntity.ok(articles); - } - - - // 글타래 수정 - // 멤버 - 장소는 수정 불가 - // 관리자 전체 가능 - @Operation(summary = "글타래 수정", description = "글타래 Id 로 글타래를 수정합니다. 사용자는 자신이 작성한 글타래 장소가 아닌 글타래만 수정 가능합니다. 관리자는 전부 수정 가능합니다.") - @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") - @PutMapping("/{articleId}") - public ResponseEntity updateArticle( - @PathVariable("articleId") Long articleId, - @RequestBody ArticleCreateRequest articleCreateRequest, - @Parameter(description = "현재 인증된 사용자 정보", required = true) - @AuthenticationPrincipal CustomMemberDetails principal) { - - ArticleResponse articleResponse = articleService.updateArticle( - articleId, - articleCreateRequest, - principal.getMemberId()); - return ResponseEntity.ok(articleResponse); - } - - // 글타래 삭제 - - // 논리 삭제 - // 멤버 - 장소는 삭제 불가 - // 관리자 전체 가능 - @Operation(summary = "글타래 논리 삭제", description = "글타래를 논리적으로 삭제합니다. 사용자는 자신이 작성한 글만 삭제할 수 있고, 관리자는 모든 글을 삭제할 수 있습니다.") - @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") - @PatchMapping("/{articleId}/deactivate") - public ResponseEntity deactivateArticle( - @PathVariable("articleId") Long articleId, - @AuthenticationPrincipal CustomMemberDetails principal) { - - Long memberId = principal.getMemberId(); - articleService.deactivateArticle(articleId, memberId); - return ResponseEntity.noContent().build(); - } - - // 물리 삭제 - // 관리자만 가능 - @Operation(summary = "글타래 물리 삭제", description = "관리자만 글타래를 물리적으로 삭제할 수 있습니다.") - @PreAuthorize("hasAuthority('ROLE_ADMIN')") - @DeleteMapping("/{articleId}") - public ResponseEntity deleteArticle( - @PathVariable("articleId") Long articleId) { - - articleService.deleteArticle(articleId); - return ResponseEntity.noContent().build(); - } + // 회원 자신이 작성한 글타래 목록 조회 // list + // is deleted false 안 보임 + @Operation(summary = "사용자가 작성한 전체 글타래 조회", description = "사용자가 자신이 작성한 글타래 목록을 조회합니다.") + @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") + @GetMapping("/myArticles") + public ResponseEntity> getArticlesByMemberId( + @Parameter(description = "현재 인증된 사용자 정보", required = true) + @AuthenticationPrincipal CustomMemberDetails principal) { + + List articles = articleService.findArticlesByMemberId(principal.getMemberId()); + return ResponseEntity.ok(articles); + } + + + // 글타래 수정 + // 멤버 - 장소는 수정 불가 + // 관리자 전체 가능 + @Operation(summary = "글타래 수정", description = "글타래 Id 로 글타래를 수정합니다. 사용자는 자신이 작성한 글타래 장소가 아닌 글타래만 수정 가능합니다. 관리자는 전부 수정 가능합니다.") + @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") + @PutMapping("/{articleId}") + public ResponseEntity updateArticle( + @PathVariable("articleId") Long articleId, + @RequestBody ArticleCreateRequest articleCreateRequest, + @Parameter(description = "현재 인증된 사용자 정보", required = true) + @AuthenticationPrincipal CustomMemberDetails principal) { + + ArticleResponse articleResponse = articleService.updateArticle( + articleId, + articleCreateRequest, + principal.getMemberId()); + return ResponseEntity.ok(articleResponse); + } + + // 글타래 삭제 + + // 논리 삭제 + // 멤버 - 장소는 삭제 불가 + // 관리자 전체 가능 + @Operation(summary = "글타래 논리 삭제", description = "글타래를 논리적으로 삭제합니다. 사용자는 자신이 작성한 글만 삭제할 수 있고, 관리자는 모든 글을 삭제할 수 있습니다.") + @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')") + @PatchMapping("/{articleId}/deactivate") + public ResponseEntity deactivateArticle( + @PathVariable("articleId") Long articleId, + @AuthenticationPrincipal CustomMemberDetails principal) { + + Long memberId = principal.getMemberId(); + articleService.deactivateArticle(articleId, memberId); + return ResponseEntity.noContent().build(); + } + + // 물리 삭제 + // 관리자만 가능 + @Operation(summary = "글타래 물리 삭제", description = "관리자만 글타래를 물리적으로 삭제할 수 있습니다.") + @PreAuthorize("hasAuthority('ROLE_ADMIN')") + @DeleteMapping("/{articleId}") + public ResponseEntity deleteArticle( + @PathVariable("articleId") Long articleId) { + + articleService.deleteArticle(articleId); + return ResponseEntity.noContent().build(); + } + + // 목격글타래 전체조회 + @Operation(summary = "목격글타래 전체조회", description = "실종글타래 ID로 관련 목격글타래를 전체 조회합니다.") + @GetMapping("/public/sighting/{articleId}") + public ResponseEntity> getSightingArticleByLostArticleId( + @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { + + //목격제보 글타래 전체조회 + List articleResponse = articleService.getSightArticleByLostId(articleId); + + return ResponseEntity.ok(articleResponse); + } } diff --git a/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java b/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java new file mode 100644 index 00000000..abbcab9c --- /dev/null +++ b/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java @@ -0,0 +1,43 @@ +package com.mallangs.domain.article.dto.request; + +import com.mallangs.domain.article.validation.ValidationGroups; +import com.mallangs.domain.pet.entity.PetGender; +import com.mallangs.domain.pet.entity.PetType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +@Getter +@ToString(callSuper = true) +@RequiredArgsConstructor +@SuperBuilder +@Schema(description = "자식 클래스 DTO", allOf = ArticleCreateRequest.class) +public class SightingArticleCreateRequest extends ArticleCreateRequest { + + @NotNull + private Long lostArticleId; + + @NotNull(message = "동물 타입은 필수입니다.", groups = ValidationGroups.CreateGroup.class) + private PetType petType; + + private String breed; + + private PetGender petGender; + + private String petColor; + + private String chipNumber; + + private LocalDateTime sightDate; + + @NotBlank(message = "목격 장소는 필수입니다.", groups = ValidationGroups.CreateGroup.class) + private String sightLocation; + +} diff --git a/src/main/java/com/mallangs/domain/article/dto/response/SightingArticleResponse.java b/src/main/java/com/mallangs/domain/article/dto/response/SightingArticleResponse.java new file mode 100644 index 00000000..3f7baccc --- /dev/null +++ b/src/main/java/com/mallangs/domain/article/dto/response/SightingArticleResponse.java @@ -0,0 +1,50 @@ +package com.mallangs.domain.article.dto.response; + +import com.mallangs.domain.article.entity.SightingArticle; +import com.mallangs.domain.article.validation.ValidationGroups; +import com.mallangs.domain.pet.entity.PetGender; +import com.mallangs.domain.pet.entity.PetType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@ToString +@RequiredArgsConstructor +public class SightingArticleResponse extends ArticleResponse { + + @NotNull + private Long lostArticleId; + + @NotNull(message = "동물 타입은 필수입니다.", groups = ValidationGroups.CreateGroup.class) + private PetType petType; + + private String breed; + + private PetGender petGender; + + private String petColor; + + private String chipNumber; + + private LocalDateTime sightDate; + + @NotBlank(message = "목격 장소는 필수입니다.", groups = ValidationGroups.CreateGroup.class) + private String sightLocation; + + public SightingArticleResponse(SightingArticle article){ + super(article); + this.lostArticleId = article.getLostArticleId(); + this.petType = article.getPetType(); + this.breed = article.getBreed(); + this.petGender = article.getPetGender(); + this.petColor = article.getPetColor(); + this.chipNumber = article.getChipNumber(); + this.sightDate = article.getSightDate(); + this.sightLocation = article.getSightLocation(); + } +} diff --git a/src/main/java/com/mallangs/domain/article/entity/ArticleType.java b/src/main/java/com/mallangs/domain/article/entity/ArticleType.java index ad401ede..45e1d9cd 100644 --- a/src/main/java/com/mallangs/domain/article/entity/ArticleType.java +++ b/src/main/java/com/mallangs/domain/article/entity/ArticleType.java @@ -15,7 +15,10 @@ public enum ArticleType { RESCUE("rescue"), @JsonProperty("place") - PLACE("place"); + PLACE("place"), + + @JsonProperty("sight") + SIGHT("sight"); private final String description; } diff --git a/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java b/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java new file mode 100644 index 00000000..e01cd654 --- /dev/null +++ b/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java @@ -0,0 +1,108 @@ +package com.mallangs.domain.article.entity; + +import com.mallangs.domain.article.dto.request.SightingArticleCreateRequest; +import com.mallangs.domain.member.entity.Member; +import com.mallangs.domain.pet.entity.PetGender; +import com.mallangs.domain.pet.entity.PetType; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@DiscriminatorValue("sighting") +@SuperBuilder +public class SightingArticle extends Article { + + @NotNull + private Long lostArticleId; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PetType petType; + + private String breed; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PetGender petGender; + + private String petColor; + + private String chipNumber; + + private LocalDateTime sightDate; + + private String sightLocation; + + @Override + public void applyChanges(Article updatedArticle) { + super.applyChanges(updatedArticle); + + if (updatedArticle instanceof SightingArticle) { + SightingArticle updatedSightingArticle = (SightingArticle) updatedArticle; + if (updatedSightingArticle.getLostArticleId() != null) { + this.lostArticleId = updatedSightingArticle.getLostArticleId(); + } + if (updatedSightingArticle.getPetType() != null) { + this.petType = updatedSightingArticle.getPetType(); + } + if (updatedSightingArticle.getBreed() != null) { + this.breed = updatedSightingArticle.getBreed(); + } + if (updatedSightingArticle.getPetGender() != null) { + this.petGender = updatedSightingArticle.getPetGender(); + } + if (updatedSightingArticle.getPetColor() != null) { + this.petColor = updatedSightingArticle.getPetColor(); + } + if (updatedSightingArticle.getChipNumber() != null) { + this.chipNumber = updatedSightingArticle.getChipNumber(); + } + if (updatedSightingArticle.getSightDate() != null) { + this.sightDate = updatedSightingArticle.getSightDate(); + } + if (updatedSightingArticle.getSightLocation() != null) { + this.sightLocation = updatedSightingArticle.getSightLocation(); + } + } + } + + public static SightingArticle createSightingArticle(Member member, SightingArticleCreateRequest createRequest) { + // GeometryFactory 객체 생성 + GeometryFactory geometryFactory = new GeometryFactory(); + + // 위도와 경도를 기반으로 Coordinate 객체 생성 + Coordinate coordinate = new Coordinate(createRequest.getLongitude(), + createRequest.getLatitude()); + + // Point 객체 생성 + Point geography = geometryFactory.createPoint(coordinate); + geography.setSRID(4326); // SRID 4326 (WGS 84) 설정 + + return SightingArticle.builder() + .petType(createRequest.getPetType()) + .breed(createRequest.getBreed()) + .petGender(createRequest.getPetGender()) + .petColor(createRequest.getPetColor()) + .chipNumber(createRequest.getChipNumber()) + .sightDate(createRequest.getSightDate()) + .sightLocation(createRequest.getSightLocation()) + .member(member) + .articleType(createRequest.getArticleType()) + .articleStatus(createRequest.getArticleStatus()) + .title(createRequest.getTitle()) + .geography(geography) + .description(createRequest.getDescription()) + .image(createRequest.getImage()) + .build(); + } +} diff --git a/src/main/java/com/mallangs/domain/article/factory/SightArticleFactory.java b/src/main/java/com/mallangs/domain/article/factory/SightArticleFactory.java new file mode 100644 index 00000000..75419b81 --- /dev/null +++ b/src/main/java/com/mallangs/domain/article/factory/SightArticleFactory.java @@ -0,0 +1,30 @@ +package com.mallangs.domain.article.factory; + +import com.mallangs.domain.article.dto.request.ArticleCreateRequest; +import com.mallangs.domain.article.dto.request.LostCreateRequest; +import com.mallangs.domain.article.dto.request.SightingArticleCreateRequest; +import com.mallangs.domain.article.dto.response.ArticleResponse; +import com.mallangs.domain.article.dto.response.LostResponse; +import com.mallangs.domain.article.dto.response.SightingArticleResponse; +import com.mallangs.domain.article.entity.Article; +import com.mallangs.domain.article.entity.LostArticle; +import com.mallangs.domain.article.entity.SightingArticle; +import com.mallangs.domain.board.dto.request.SightingCreateRequest; +import com.mallangs.domain.member.entity.Member; +import org.springframework.stereotype.Component; + +@Component +public class SightArticleFactory implements ArticleFactory { + + @Override + public Article createArticle(Member member, ArticleCreateRequest request) { + return SightingArticle.createSightingArticle(member, (SightingArticleCreateRequest) request); + } + + @Override + public SightingArticleResponse createResponse(Article article) { + SightingArticle sightArticle = (SightingArticle) article; + return new SightingArticleResponse(sightArticle); + } + +} \ No newline at end of file diff --git a/src/main/java/com/mallangs/domain/article/repository/ArticleRepository.java b/src/main/java/com/mallangs/domain/article/repository/ArticleRepository.java index 4dc1a07b..ed8415db 100644 --- a/src/main/java/com/mallangs/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/mallangs/domain/article/repository/ArticleRepository.java @@ -49,5 +49,10 @@ List
findByTitleContainingOrDescriptionContainingAndMapVisibility( @Param("title") String title, @Param("description") String description); + // 모든 사용자 + // 모든 목격글타래 조회 + // article 에서 map visible // jpql 은 join 필요 없음 + @Query("SELECT s FROM SightingArticle s WHERE s.lostArticleId = :lostSightId AND s.mapVisibility = 'VISIBLE'") + List
findSightingArticles(@Param("lostSightId") Long lostSightId); } diff --git a/src/main/java/com/mallangs/domain/article/service/ArticleService.java b/src/main/java/com/mallangs/domain/article/service/ArticleService.java index 7bd9e0ea..ac6d3980 100644 --- a/src/main/java/com/mallangs/domain/article/service/ArticleService.java +++ b/src/main/java/com/mallangs/domain/article/service/ArticleService.java @@ -15,9 +15,11 @@ import com.mallangs.domain.member.repository.MemberRepository; import com.mallangs.global.exception.ErrorCode; import com.mallangs.global.exception.MallangsCustomException; + import java.util.List; import java.util.Objects; import java.util.stream.Collectors; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -29,305 +31,379 @@ @RequiredArgsConstructor public class ArticleService { - private final MemberRepository memberRepository; - private final ArticleFactoryManager factoryManager; - private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + private final ArticleFactoryManager factoryManager; + private final ArticleRepository articleRepository; + + public ArticleResponse createArticle(ArticleCreateRequest articleCreateRequest, Long memberId) { + Member foundMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); - public ArticleResponse createArticle(ArticleCreateRequest articleCreateRequest, Long memberId) { - Member foundMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); + // 팩토리 매니저를 통해 적절한 팩토리 선택 + log.info(articleCreateRequest.toString()); + log.info(articleCreateRequest.getArticleType().getDescription()); + ArticleFactory factory = factoryManager.getFactory( + articleCreateRequest.getArticleType().getDescription()); - // 팩토리 매니저를 통해 적절한 팩토리 선택 - log.info(articleCreateRequest.toString()); - log.info(articleCreateRequest.getArticleType().getDescription()); - ArticleFactory factory = factoryManager.getFactory( - articleCreateRequest.getArticleType().getDescription()); + // 팩토리에서 article 생성 + Article article = factory.createArticle(foundMember, articleCreateRequest); + article.hideInMap(); // published 상태 아니면 map hidden + + Article savedArticle = articleRepository.save(article); + + // 각 타입에 맞는 Response 객체 반환 + return factory.createResponse(savedArticle); + } - // 팩토리에서 article 생성 - Article article = factory.createArticle(foundMember, articleCreateRequest); - article.hideInMap(); // published 상태 아니면 map hidden - Article savedArticle = articleRepository.save(article); +// // 글타래 단건 조회 +// // 사용자는 map visiblie 인 경우 +// public ArticleResponse getArticleById(Long articleId, String userRole, Long memberId) { +// Article foundArticle = articleRepository.findById(articleId) +// .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); +// +// // ArticleResponse 로 반환 통일되게 팩토리메서드 작성 필요 +// ArticleFactory factory = factoryManager.getFactory( +// foundArticle.getArticleType().getDescription()); +// log.info("factory : {}",factory); +// +// if (MemberRole.ROLE_ADMIN.name().equals(userRole)) { +// log.info("단건 조회 ADMIN{}", userRole); +// ArticleResponse response = factory.createResponse(foundArticle); +// +// log.info("단건조회결과1 : {}",response); +// return response; +// } +// +// if (MemberRole.ROLE_USER.name().equals(userRole)) { // 논리 삭제하지 않은 자신의 글이거나 mapVisible +// if (Objects.equals(foundArticle.getMember().getMemberId(), memberId) +// && !foundArticle.getIsDeleted() +// || foundArticle.getMapVisibility() == MapVisibility.VISIBLE) { +// ArticleResponse response = factory.createResponse(foundArticle); +// log.info("단건조회결과2 : {}",response); +// +// return response; +// } +// log.info("단건 조회 USER ACCESS DENIED{}", userRole); +// } +// +// // 비회원 +// if ("ROLE_GUEST".equals(userRole) && foundArticle.getMapVisibility() == MapVisibility.VISIBLE) { +// log.info("단건 조회 NOT MEMBER{}", userRole); +// ArticleResponse response = factory.createResponse(foundArticle); +// log.info("단건조회결과3 : {}",response); +// +// return response; +// } else { +// log.error("비회원 조회 불가"); +// } +// +// log.info("단건 조회 OTHER" + userRole); +// throw new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND); +// } + + // 글타래 단건 조회 (수정본) + // 사용자는 map visiblie 인 경우 + public ArticleResponse getArticleById(Long articleId, String userRole, Long memberId) { + Article foundArticle = articleRepository.findById(articleId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); + + // ArticleResponse 로 반환 통일되게 팩토리메서드 작성 필요 + ArticleFactory factory = factoryManager.getFactory( + foundArticle.getArticleType().getDescription()); + log.info("factory : {}", factory); + + if (MemberRole.ROLE_ADMIN.name().equals(userRole)) { + log.info("단건 조회 ADMIN{}", userRole); + ArticleResponse response = factory.createResponse(foundArticle); + + log.info("단건조회결과1 : {}", response); + return response; + } + + if (MemberRole.ROLE_USER.name().equals(userRole)) { // 논리 삭제하지 않은 자신의 글이거나 mapVisible + if (Objects.equals(foundArticle.getMember().getMemberId(), memberId) + && !foundArticle.getIsDeleted() + || foundArticle.getMapVisibility() == MapVisibility.VISIBLE) { + ArticleResponse response = factory.createResponse(foundArticle); + log.info("단건조회결과2 : {}", response); + + return response; + } + log.info("단건 조회 USER ACCESS DENIED{}", userRole); + } + + // 비회원 + if ("ROLE_GUEST".equals(userRole) && foundArticle.getMapVisibility() == MapVisibility.VISIBLE) { + log.info("단건 조회 NOT MEMBER{}", userRole); + ArticleResponse response = factory.createResponse(foundArticle); + log.info("단건조회결과3 : {}", response); + + return response; + } + + log.info("단건 조회 OTHER" + userRole); + throw new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND); + } - // 각 타입에 맞는 Response 객체 반환 - return factory.createResponse(savedArticle); - } + // 목격제보 글타래 전체조회 + public List getSightArticleByLostId(Long LostArticleId) { + List
articles = articleRepository.findSightingArticles(LostArticleId); + + // ArticleResponse 로 반환 통일되게 팩토리메서드 작성 필요 + try { + return articles.stream() + .map(article -> { + ArticleFactory factory = factoryManager.getFactory( + article.getArticleType().getDescription()); + return factory.createResponse(article); + }) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("목격제보 글타래 전체조회 실패 {}", e.getMessage()); + throw new MallangsCustomException(ErrorCode.INVALID_REQUEST); + } + } - // 글타래 단건 조회 - // 사용자는 map visiblie 인 경우 - public ArticleResponse getArticleById(Long articleId, String userRole, Long memberId) { - Article foundArticle = articleRepository.findById(articleId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); + // 관리자만 + // 글타래 전체 조회 // 지도가 아닌 경우 + public ArticlePageResponse findAllTypeArticles(Pageable pageable) { + Page
articles = articleRepository.findAll(pageable); - // ArticleResponse 로 반환 통일되게 팩토리메서드 작성 필요 - ArticleFactory factory = factoryManager.getFactory( - foundArticle.getArticleType().getDescription()); + List articleResponses = articles.map(article -> { + ArticleFactory factory = factoryManager.getFactory(article.getArticleType().getDescription()); + return factory.createResponse(article); + }).getContent(); - if (MemberRole.ROLE_ADMIN.name().equals(userRole)) { - log.info("단건 조회 ADMIN{}", userRole); - return factory.createResponse(foundArticle); + return new ArticlePageResponse( + articleResponses, + articles.getTotalElements(), + articles.getTotalPages(), + articles.getNumber(), + articles.getSize() + ); } - if (MemberRole.ROLE_USER.name().equals(userRole)) { // 논리 삭제하지 않은 자신의 글이거나 mapVisible - if (Objects.equals(foundArticle.getMember().getMemberId(), memberId) - && !foundArticle.getIsDeleted() - || foundArticle.getMapVisibility() == MapVisibility.VISIBLE) { - return factory.createResponse(foundArticle); - } - log.info("단건 조회 USER ACCESS DENIED{}", userRole); + // 관리자 목록조회 + // 글타래 타입 별 전체/실종/구조 조회 // 장소 카테고리도 설정 가능? // 대분류 + public ArticlePageResponse findArticlesByArticleType(Pageable pageable, + String articleType) { + + if (Objects.equals(articleType, "place") || Objects.equals(articleType, "user")) { + // 장소, 사용자 등록 위치 + boolean isPublicData; + isPublicData = Objects.equals("place", articleType); // place 면 isPublicData = true + + Page
articles = articleRepository.findPlaceArticlesByType(pageable, isPublicData); + + List articleResponses = articles.map(article -> { + ArticleFactory factory = factoryManager.getFactory("place"); + return factory.createResponse(article); + }).getContent(); + + return new ArticlePageResponse( + articleResponses, + articles.getTotalElements(), + articles.getTotalPages(), + articles.getNumber(), + articles.getSize() + ); + } + + // 실종, 구조 + ArticleType type = ArticleType.valueOf(articleType.toUpperCase()); + + Page
articles = articleRepository.findByArticleType(pageable, type); + + List articleResponses = articles.map(article -> { + ArticleFactory factory = factoryManager.getFactory(article.getArticleType().getDescription()); + return factory.createResponse(article); + }).getContent(); + + return new ArticlePageResponse( + articleResponses, + articles.getTotalElements(), + articles.getTotalPages(), + articles.getNumber(), + articles.getSize() + ); } - // 비회원 - if ("ROLE_GUEST".equals(userRole) && foundArticle.getMapVisibility() == MapVisibility.VISIBLE) { - log.info("단건 조회 NOT MEMBER{}", userRole); - return factory.createResponse(foundArticle); - } else { - log.error("비회원 조회 불가"); + + // 실종 글타래 + // map visibility 기준 + public List findLostArticles(CaseStatus lostStatus) { + List
articles = articleRepository.findLostArticles(lostStatus); + + return articles.stream() + .map(article -> { + ArticleFactory factory = factoryManager.getFactory( + article.getArticleType().getDescription()); + return factory.createResponse(article); + }) + .collect(Collectors.toList()); } - log.info("단건 조회 OTHER" + userRole); - throw new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND); - } - - - // 관리자만 - // 글타래 전체 조회 // 지도가 아닌 경우 - public ArticlePageResponse findAllTypeArticles(Pageable pageable) { - Page
articles = articleRepository.findAll(pageable); - - List articleResponses = articles.map(article -> { - ArticleFactory factory = factoryManager.getFactory(article.getArticleType().getDescription()); - return factory.createResponse(article); - }).getContent(); - - return new ArticlePageResponse( - articleResponses, - articles.getTotalElements(), - articles.getTotalPages(), - articles.getNumber(), - articles.getSize() - ); - } - - // 관리자 목록조회 - // 글타래 타입 별 전체/실종/구조 조회 // 장소 카테고리도 설정 가능? // 대분류 - public ArticlePageResponse findArticlesByArticleType(Pageable pageable, - String articleType) { - - if (Objects.equals(articleType, "place") || Objects.equals(articleType, "user")) { - // 장소, 사용자 등록 위치 - boolean isPublicData; - isPublicData = Objects.equals("place", articleType); // place 면 isPublicData = true - - Page
articles = articleRepository.findPlaceArticlesByType(pageable, isPublicData); - - List articleResponses = articles.map(article -> { - ArticleFactory factory = factoryManager.getFactory("place"); - return factory.createResponse(article); - }).getContent(); - - return new ArticlePageResponse( - articleResponses, - articles.getTotalElements(), - articles.getTotalPages(), - articles.getNumber(), - articles.getSize() - ); + // 관리자 + // 장소 세부 카테고리 있는 것 + public ArticlePageResponse findPlaceArticlesByCategory(Pageable pageable, + String articleType, String placeCategory) { + boolean isPublicData; + isPublicData = Objects.equals("place", articleType); // place 인 경우 + Page
articles = articleRepository.findPlaceArticlesByCategory(pageable, isPublicData, + placeCategory); + + List articleResponses = articles.map(article -> { + ArticleFactory factory = factoryManager.getFactory("place"); + return factory.createResponse(article); + }).getContent(); + + return new ArticlePageResponse( + articleResponses, + articles.getTotalElements(), + articles.getTotalPages(), + articles.getNumber(), + articles.getSize() + ); + } - // 실종, 구조 - ArticleType type = ArticleType.valueOf(articleType.toUpperCase()); - - Page
articles = articleRepository.findByArticleType(pageable, type); - - List articleResponses = articles.map(article -> { - ArticleFactory factory = factoryManager.getFactory(article.getArticleType().getDescription()); - return factory.createResponse(article); - }).getContent(); - - return new ArticlePageResponse( - articleResponses, - articles.getTotalElements(), - articles.getTotalPages(), - articles.getNumber(), - articles.getSize() - ); - } - - - // 실종 글타래 - // map visibility 기준 - public List findLostArticles(CaseStatus lostStatus) { - List
articles = articleRepository.findLostArticles(lostStatus); - - return articles.stream() - .map(article -> { - ArticleFactory factory = factoryManager.getFactory( - article.getArticleType().getDescription()); - return factory.createResponse(article); - }) - .collect(Collectors.toList()); - } - - // 관리자 - // 장소 세부 카테고리 있는 것 - public ArticlePageResponse findPlaceArticlesByCategory(Pageable pageable, - String articleType, String placeCategory) { - boolean isPublicData; - isPublicData = Objects.equals("place", articleType); // place 인 경우 - Page
articles = articleRepository.findPlaceArticlesByCategory(pageable, isPublicData, - placeCategory); - - List articleResponses = articles.map(article -> { - ArticleFactory factory = factoryManager.getFactory("place"); - return factory.createResponse(article); - }).getContent(); - - return new ArticlePageResponse( - articleResponses, - articles.getTotalElements(), - articles.getTotalPages(), - articles.getNumber(), - articles.getSize() - ); - - } - - - // 글타래 멤버 개인 글타래 목록 조회 - // 논리 삭제 안된 것 조회 - public List findArticlesByMemberId(Long memberId) { - List
articles = articleRepository.findByMemberId(memberId); - - return articles.stream() - .map(article -> { - ArticleFactory factory = factoryManager.getFactory( - article.getArticleType().getDescription()); - return factory.createResponse(article); - }) - .collect(Collectors.toList()); - } - - // 검색어 기준 - // 지도 표시 여부 체크 // 사용자, 관리자 모두 지도 표시 여부로 확인 - public List findArticlesByKeyword(String keyword) { - List
articles = articleRepository.findByTitleContainingOrDescriptionContainingAndMapVisibility( - keyword, - keyword); - - return articles.stream() - .map(article -> { - ArticleFactory factory = factoryManager.getFactory( - article.getArticleType().getDescription()); - return factory.createResponse(article); - }) - .collect(Collectors.toList()); - } - - - // 수정 자신의 글타래 (장소는 자신의 글도 수정 삭제 불가) // 관리자는 수정 가능 - public ArticleResponse updateArticle(Long articleId, ArticleCreateRequest articleUpdateRequest, - Long memberId) { - Member foundMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); - - // 기존 Article 조회 - Article foundArticle = articleRepository.findById(articleId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); - // 자신의 글인지 체크 - // 장소인지 체크 - // foundMember role 체크, foundArticle 타입 체크 - if (foundMember.getMemberRole() == MemberRole.ROLE_USER) { - validateUserArticleAccess(foundArticle, memberId, false); + // 글타래 멤버 개인 글타래 목록 조회 + // 논리 삭제 안된 것 조회 + public List findArticlesByMemberId(Long memberId) { + List
articles = articleRepository.findByMemberId(memberId); + + return articles.stream() + .map(article -> { + ArticleFactory factory = factoryManager.getFactory( + article.getArticleType().getDescription()); + return factory.createResponse(article); + }) + .collect(Collectors.toList()); } - // 글타래 타입 불변인지 체크 - if (!Objects.equals(foundArticle.getArticleType().getDescription(), - articleUpdateRequest.getArticleType().getDescription())) { - throw new MallangsCustomException(ErrorCode.INVALID_TYPE_CHANGE); + // 검색어 기준 + // 지도 표시 여부 체크 // 사용자, 관리자 모두 지도 표시 여부로 확인 + public List findArticlesByKeyword(String keyword) { + List
articles = articleRepository.findByTitleContainingOrDescriptionContainingAndMapVisibility( + keyword, + keyword); + + return articles.stream() + .map(article -> { + ArticleFactory factory = factoryManager.getFactory( + article.getArticleType().getDescription()); + return factory.createResponse(article); + }) + .collect(Collectors.toList()); } - ArticleFactory factory = factoryManager.getFactory( - foundArticle.getArticleType().getDescription()); - Article updatedArticle = factory.createArticle(foundArticle.getMember(), - articleUpdateRequest); - foundArticle.applyChanges(updatedArticle); + // 수정 자신의 글타래 (장소는 자신의 글도 수정 삭제 불가) // 관리자는 수정 가능 + public ArticleResponse updateArticle(Long articleId, ArticleCreateRequest articleUpdateRequest, + Long memberId) { + Member foundMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); + + // 기존 Article 조회 + Article foundArticle = articleRepository.findById(articleId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); + + // 자신의 글인지 체크 + // 장소인지 체크 + // foundMember role 체크, foundArticle 타입 체크 + if (foundMember.getMemberRole() == MemberRole.ROLE_USER) { + validateUserArticleAccess(foundArticle, memberId, false); + } - // 글 상태가 발행이 아닌 경우 - // mapVisibility 상태 변경 - // 사용자 자신의 글타래에서는 볼 수 있으나 지도에는 안뜸 - foundArticle.hideInMap(); + // 글타래 타입 불변인지 체크 + if (!Objects.equals(foundArticle.getArticleType().getDescription(), + articleUpdateRequest.getArticleType().getDescription())) { + throw new MallangsCustomException(ErrorCode.INVALID_TYPE_CHANGE); + } - Article savedArticle = articleRepository.save(foundArticle); + ArticleFactory factory = factoryManager.getFactory( + foundArticle.getArticleType().getDescription()); + Article updatedArticle = factory.createArticle(foundArticle.getMember(), + articleUpdateRequest); - return factory.createResponse(savedArticle); - } + foundArticle.applyChanges(updatedArticle); - // 논리 삭제 - // 사용자 삭제 인 경우 - 데이터에는 남아있음 - // 사용자 논리 삭제 시 map invisible 로 변경, isDeleted 변경 - public void deactivateArticle(Long articleId, Long memberId) { - Member foundMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); + // 글 상태가 발행이 아닌 경우 + // mapVisibility 상태 변경 + // 사용자 자신의 글타래에서는 볼 수 있으나 지도에는 안뜸 + foundArticle.hideInMap(); - Article foundArticle = articleRepository.findById(articleId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); + Article savedArticle = articleRepository.save(foundArticle); - if (foundMember.getMemberRole() == MemberRole.ROLE_USER) { - validateUserArticleAccess(foundArticle, memberId, true); + return factory.createResponse(savedArticle); } - // 논리 삭제 처리 상태 변경 - foundArticle.deactivate(); // 사용자 못봄 - articleRepository.save(foundArticle); + // 논리 삭제 + // 사용자 삭제 인 경우 - 데이터에는 남아있음 + // 사용자 논리 삭제 시 map invisible 로 변경, isDeleted 변경 + public void deactivateArticle(Long articleId, Long memberId) { + Member foundMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); - } + Article foundArticle = articleRepository.findById(articleId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); + if (foundMember.getMemberRole() == MemberRole.ROLE_USER) { + validateUserArticleAccess(foundArticle, memberId, true); + } - // 물리 삭제 // 관리자 - public void deleteArticle(Long articleId) { - Article foundArticle = articleRepository.findById(articleId) - .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); + // 논리 삭제 처리 상태 변경 + foundArticle.deactivate(); // 사용자 못봄 + articleRepository.save(foundArticle); - articleRepository.deleteById(foundArticle.getId()); - } + } - private void validateUserArticleAccess(Article foundArticle, Long memberId, Boolean forDelete) { - validateArticleAccess(foundArticle, memberId, forDelete); - validateUserAccessByArticleType(foundArticle, forDelete); - } + // 물리 삭제 // 관리자 + public void deleteArticle(Long articleId) { + Article foundArticle = articleRepository.findById(articleId) + .orElseThrow(() -> new MallangsCustomException(ErrorCode.ARTICLE_NOT_FOUND)); - // 자신의 글인지 체크 - private void validateArticleAccess(Article foundArticle, Long memberId, Boolean forAnyDelete) { - if (forAnyDelete) { - if (!Objects.equals(foundArticle.getMember().getMemberId(), memberId)) { - throw new MallangsCustomException(ErrorCode.UNAUTHORIZED_DELETE); - } - } else { - if (!Objects.equals(foundArticle.getMember().getMemberId(), memberId)) { - throw new MallangsCustomException(ErrorCode.UNAUTHORIZED_MODIFY); - } + articleRepository.deleteById(foundArticle.getId()); } - } - - // 장소 접근 체크 - private void validateUserAccessByArticleType(Article foundArticle, Boolean forAnyDelete) { - String articleType = foundArticle.getArticleType().getDescription(); - if (forAnyDelete) { - if (Objects.equals(articleType, "place")) { - throw new MallangsCustomException(ErrorCode.RESOURCE_NOT_DELETABLE); - } - } else { - if (Objects.equals(articleType, "place")) { - throw new MallangsCustomException(ErrorCode.RESOURCE_NOT_MODIFIABLE); - } + + + private void validateUserArticleAccess(Article foundArticle, Long memberId, Boolean forDelete) { + validateArticleAccess(foundArticle, memberId, forDelete); + validateUserAccessByArticleType(foundArticle, forDelete); } - } + // 자신의 글인지 체크 + private void validateArticleAccess(Article foundArticle, Long memberId, Boolean forAnyDelete) { + if (forAnyDelete) { + if (!Objects.equals(foundArticle.getMember().getMemberId(), memberId)) { + throw new MallangsCustomException(ErrorCode.UNAUTHORIZED_DELETE); + } + } else { + if (!Objects.equals(foundArticle.getMember().getMemberId(), memberId)) { + throw new MallangsCustomException(ErrorCode.UNAUTHORIZED_MODIFY); + } + } + } + + // 장소 접근 체크 + private void validateUserAccessByArticleType(Article foundArticle, Boolean forAnyDelete) { + String articleType = foundArticle.getArticleType().getDescription(); + if (forAnyDelete) { + if (Objects.equals(articleType, "place")) { + throw new MallangsCustomException(ErrorCode.RESOURCE_NOT_DELETABLE); + } + } else { + if (Objects.equals(articleType, "place")) { + throw new MallangsCustomException(ErrorCode.RESOURCE_NOT_MODIFIABLE); + } + } + + } } diff --git a/src/main/java/com/mallangs/domain/board/controller/BoardController.java b/src/main/java/com/mallangs/domain/board/controller/BoardController.java index 4b06c34d..6f476fc5 100644 --- a/src/main/java/com/mallangs/domain/board/controller/BoardController.java +++ b/src/main/java/com/mallangs/domain/board/controller/BoardController.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.*; import java.net.URI; +import java.util.List; @Tag(name = "커뮤니티 & 실종신고-목격제보 API", description = "커뮤니티/실종신고-목격제보 관련 API") @RestController @@ -215,4 +216,5 @@ public ResponseEntity changePostsStatus(@RequestBody @Valid AdminBoardStat boardService.changeBoardStatus(request.getBoardIds(), request.getStatus()); return ResponseEntity.noContent().build(); } + } diff --git a/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java b/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java index ba49e72f..6fc0c8de 100644 --- a/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java +++ b/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java @@ -12,6 +12,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface BoardRepository extends JpaRepository { @@ -21,6 +23,9 @@ public interface BoardRepository extends JpaRepository { """) Page findAllByBoardType(@Param("boardType") BoardType boardType, Pageable pageable); + //실증글타래 List로 조회 + List findAllByBoardType(@Param("boardType") BoardType boardType); + // 카테고리 이름으로 게시글 조회 @Query(""" SELECT b FROM Board b WHERE b.category.name = :name AND b.boardStatus = 'PUBLISHED' AND b.boardType = :boardType ORDER BY b.createdAt DESC diff --git a/src/main/java/com/mallangs/domain/board/service/BoardService.java b/src/main/java/com/mallangs/domain/board/service/BoardService.java index 377ea098..73713d07 100644 --- a/src/main/java/com/mallangs/domain/board/service/BoardService.java +++ b/src/main/java/com/mallangs/domain/board/service/BoardService.java @@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; @Service @Transactional(readOnly = true) @@ -233,4 +234,9 @@ public boolean isNotAdminRole() { return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() .noneMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); } + + // 실종신고-목격제보 게시판 게시글 전체 조회 (실종글타래랑 비교위함 메서드) + public List getAllSightingBoardToList() { + return boardRepository.findAllByBoardType(BoardType.SIGHTING).stream().map(SightingListResponse::new).collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/src/main/java/com/mallangs/domain/chat/controller/ChatRoomController.java b/src/main/java/com/mallangs/domain/chat/controller/ChatRoomController.java index df61d56e..368eeee1 100644 --- a/src/main/java/com/mallangs/domain/chat/controller/ChatRoomController.java +++ b/src/main/java/com/mallangs/domain/chat/controller/ChatRoomController.java @@ -34,8 +34,7 @@ public class ChatRoomController { @Operation(summary = "채팅방 생성", description = "채팅방을 생성합니다.") @ApiResponses({ @ApiResponse(responseCode = "201", description = "채팅방 생성 성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청"), - @ApiResponse(responseCode = "401", description = "인증 실패"), + @ApiResponse(responseCode = "400", description = "잘못된 요청") }) public ResponseEntity create(@PathVariable("memberId") Long memberId, @AuthenticationPrincipal CustomMemberDetails customMemberDetails) { @@ -84,10 +83,10 @@ public ResponseEntity delete(@PathVariable("participatedRoomId") Long partici } - //채팅방 조회 + //참여 채팅방 조회 @ResponseBody @GetMapping("/{participatedRoomId}") - @Operation(summary = "채팅방 조회", description = "채팅방을 조회합니다.") + @Operation(summary = "참여 채팅방 조회", description = "참여 채팅방을 조회합니다.") @ApiResponses({ @ApiResponse(responseCode = "201", description = "조회 성공"), @ApiResponse(responseCode = "400", description = "채팅방이 존재하지 않습니다.") diff --git a/src/main/java/com/mallangs/domain/chat/dto/response/ChatMessageListResponse.java b/src/main/java/com/mallangs/domain/chat/dto/response/ChatMessageListResponse.java index 4c19731f..b8a90c2a 100644 --- a/src/main/java/com/mallangs/domain/chat/dto/response/ChatMessageListResponse.java +++ b/src/main/java/com/mallangs/domain/chat/dto/response/ChatMessageListResponse.java @@ -11,8 +11,6 @@ public class ChatMessageListResponse { private Long chatMessageId; private Long chatRoomId; - private String profileImage; - private String chatMessageImage; private String sender; private String message; private MessageType type; @@ -21,13 +19,10 @@ public class ChatMessageListResponse { @Builder public ChatMessageListResponse(Long chatMessageId, Long chatRoomId, - String profileImage, String chatMessageImage, String sender, String message, MessageType type, Boolean senderRead, Boolean receiverRead) { this.chatMessageId = chatMessageId; this.chatRoomId = chatRoomId; - this.profileImage = profileImage; - this.chatMessageImage = chatMessageImage; this.sender = sender; this.message = message; this.type = type; diff --git a/src/main/java/com/mallangs/domain/chat/dto/response/ChatRoomResponse.java b/src/main/java/com/mallangs/domain/chat/dto/response/ChatRoomResponse.java index 1ec5dd8a..fb1609e4 100644 --- a/src/main/java/com/mallangs/domain/chat/dto/response/ChatRoomResponse.java +++ b/src/main/java/com/mallangs/domain/chat/dto/response/ChatRoomResponse.java @@ -9,14 +9,14 @@ public class ChatRoomResponse { private String chatRoomName; private String memberNickname; - private String memberProfileUrl; + private Long memberId; private Integer changedIsRead; @Builder - public ChatRoomResponse(String chatRoomName, String memberNickname, String memberProfileUrl, Integer changedIsRead) { + public ChatRoomResponse(String chatRoomName, String memberNickname, Long memberId, Integer changedIsRead) { this.chatRoomName = chatRoomName; this.memberNickname = memberNickname; - this.memberProfileUrl = memberProfileUrl; + this.memberId = memberId; this.changedIsRead = changedIsRead; } } diff --git a/src/main/java/com/mallangs/domain/chat/service/ChatMessageService.java b/src/main/java/com/mallangs/domain/chat/service/ChatMessageService.java index df9e44bd..abf6b2fa 100644 --- a/src/main/java/com/mallangs/domain/chat/service/ChatMessageService.java +++ b/src/main/java/com/mallangs/domain/chat/service/ChatMessageService.java @@ -116,9 +116,7 @@ public Page getPage(PageRequestDTO pageRequestDTO, Long .chatMessageId(chatMessage.getChatMessageId()) .chatRoomId(chatMessage.getChatRoom().getChatRoomId()) .message(chatMessage.getMessage()) - .chatMessageImage(chatMessage.getImageUrl()) .sender(chatMessage.getSender().getNickname().getValue()) - .profileImage(chatMessage.getSender().getProfileImage()) .type(chatMessage.getType()) .senderRead(chatMessage.getSenderRead()) .receiverRead(chatMessage.getSenderRead()) @@ -151,7 +149,7 @@ public ChatRoomResponse changeUnReadToRead(Long participatedRoomId, String nickn return ChatRoomResponse.builder() .chatRoomName(foundPartRoom.getRoomName()) .memberNickname(foundPartRoom.getParticipant().getNickname().getValue()) - .memberProfileUrl(foundPartRoom.getParticipant().getProfileImage()) + .memberId(foundPartRoom.getParticipant().getMemberId()) .changedIsRead(numChanged) .build(); } diff --git a/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java b/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java index 0bcefb16..ded4797c 100644 --- a/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java +++ b/src/main/java/com/mallangs/domain/chat/service/ChatRoomService.java @@ -85,7 +85,7 @@ public Long create(Long myId, Long partnerId) { ChatRoomResponse chatRoomResponse = ChatRoomResponse.builder() .chatRoomName(partner.getNickname().getValue()) .memberNickname(me.getNickname().getValue()) - .memberProfileUrl(me.getProfileImage()) + .memberId(me.getMemberId()) .changedIsRead(0) .build(); emitter.send(chatRoomResponse); diff --git a/src/main/java/com/mallangs/global/exception/ErrorCode.java b/src/main/java/com/mallangs/global/exception/ErrorCode.java index c77ec4c1..fd2ff90e 100644 --- a/src/main/java/com/mallangs/global/exception/ErrorCode.java +++ b/src/main/java/com/mallangs/global/exception/ErrorCode.java @@ -1,17 +1,11 @@ package com.mallangs.global.exception; -import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.CONFLICT; -import static org.springframework.http.HttpStatus.FORBIDDEN; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; -import static org.springframework.http.HttpStatus.NOT_ACCEPTABLE; -import static org.springframework.http.HttpStatus.NOT_FOUND; -import static org.springframework.http.HttpStatus.UNAUTHORIZED; - import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import static org.springframework.http.HttpStatus.*; + @Getter @RequiredArgsConstructor public enum ErrorCode { @@ -53,6 +47,7 @@ public enum ErrorCode { RESOURCE_NOT_MODIFIABLE(FORBIDDEN, "이 리소스는 수정할 수 없습니다."), RESOURCE_NOT_DELETABLE(FORBIDDEN, "이 리소스는 삭제할 수 없습니다."), INVALID_TYPE_CHANGE(BAD_REQUEST, "글타래 타입은 생성 시에 설정된 타입을 유지해야 합니다."), + INVALID_REQUEST(BAD_REQUEST, "잘못된 입력값입니다. 입력값을 확인해주세요"), // Board CATEGORY_NOT_FOUND(NOT_FOUND, "카테고리 정보를 찾을 수 없습니다."), @@ -115,8 +110,11 @@ public enum ErrorCode { REFRESH_TOKEN_EXPIRED(UNAUTHORIZED, "리프레시 토큰이 만료되었습니다."), TOKEN_EXPIRED(UNAUTHORIZED, "토큰이 만료되었습니다."), TOKEN_IS_BLOCKED(UNAUTHORIZED, "토큰이 차단되었습니다."), - REFRESH_TOKEN_MISSING(NOT_FOUND, "리프레시 토큰이 존재하지 않습니다."); + REFRESH_TOKEN_MISSING(NOT_FOUND, "리프레시 토큰이 존재하지 않습니다."), + //OpenAI API + FAILED_TO_REQUEST_OPENAI(BAD_REQUEST, "잘못된 입력입니다."), + INSUFFICIENT_OPENAI_QUOTA(TOO_MANY_REQUESTS, "OPENAI 사용량이 초과되었습니다."); private final HttpStatus httpStatus; private final String message; } \ No newline at end of file From fb9f852b6aa08133058a252e11e2b6823c3e4ab6 Mon Sep 17 00:00:00 2001 From: DongWooKim4343 Date: Tue, 24 Dec 2024 15:24:39 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[del]=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PlaceArticleCsvService.java | 4 ---- src/main/resources/application-dev.properties | 23 ------------------- 2 files changed, 27 deletions(-) delete mode 100644 src/main/resources/application-dev.properties diff --git a/src/main/java/com/mallangs/domain/article/service/PlaceArticleCsvService.java b/src/main/java/com/mallangs/domain/article/service/PlaceArticleCsvService.java index 62115035..180954c6 100644 --- a/src/main/java/com/mallangs/domain/article/service/PlaceArticleCsvService.java +++ b/src/main/java/com/mallangs/domain/article/service/PlaceArticleCsvService.java @@ -99,10 +99,6 @@ // String closeDays = line[18]; // String webSite = line[17]; // -// boolean exists = placeArticleRepository.existsByTitle(title); -// if (exists) { -// return null; // 이미 존재하면 null 반환 -// } // // return PlaceArticle.builder() // .member(member) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties deleted file mode 100644 index 25a29642..00000000 --- a/src/main/resources/application-dev.properties +++ /dev/null @@ -1,23 +0,0 @@ -# Datasource 설정 -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:test -spring.datasource.username=sa -spring.datasource.password= -spring.sql.init.mode=always - -#http://localhost:8080/h2-console 여기로 접속하여 확인 -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console - -# H2 데이터베이스 방언 설정 - -spring.data.redis.host=localhost -spring.data.redis.port=6379 - -# Hibernate ?? (??) -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.show-sql=true -spring.jpa.hibernate.ddl-auto=update - -spring.jpa.properties.hibernate.jdbc.time_zone=Asia/Seoul - From a75a894561162bf842f4d0f47df5aed3839ac8a4 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Tue, 24 Dec 2024 23:56:48 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[feat]=20AI=20=EB=AA=A9=EA=B2=A9=EC=A0=9C?= =?UTF-8?q?=EB=B3=B4,=20=EC=8B=A4=EC=A2=85=EA=B8=80=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../domain/ai/AIPromptController.java | 146 ++++++++++++------ .../domain/ai/dto/SightAIResponse.java | 34 ++++ .../article/controller/ArticleController.java | 1 + .../board/repository/BoardRepository.java | 2 +- 5 files changed, 138 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/mallangs/domain/ai/dto/SightAIResponse.java diff --git a/build.gradle b/build.gradle index 41fe7479..8cf4a149 100644 --- a/build.gradle +++ b/build.gradle @@ -108,6 +108,7 @@ dependencies { //AI prompt implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter:1.0.0-SNAPSHOT' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } jar { diff --git a/src/main/java/com/mallangs/domain/ai/AIPromptController.java b/src/main/java/com/mallangs/domain/ai/AIPromptController.java index 9712db33..b8536e80 100644 --- a/src/main/java/com/mallangs/domain/ai/AIPromptController.java +++ b/src/main/java/com/mallangs/domain/ai/AIPromptController.java @@ -1,8 +1,12 @@ package com.mallangs.domain.ai; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.mallangs.domain.ai.dto.AiResponseDTO; +import com.mallangs.domain.ai.dto.SightAIResponse; import com.mallangs.domain.article.dto.response.ArticleResponse; +import com.mallangs.domain.article.dto.response.LostResponse; import com.mallangs.domain.article.service.ArticleService; import com.mallangs.domain.board.dto.response.SightingListResponse; import com.mallangs.domain.board.service.BoardService; @@ -28,58 +32,18 @@ @RequestMapping("/api/v1/ai") @RequiredArgsConstructor public class AIPromptController { + private final VertexAiGeminiChatModel vertexAiGeminiChatModel; private final ArticleService articleService; private final BoardService boardService; - @GetMapping("/chat") - public ResponseEntity chat() { - try { - //질문내용 - String message = "{ \"response\": \"너는 누구야?\", \"details\": \"json 형식으로 답변해줘\" }"; - - // 1. Gemini에게 질문 (질문 -> 응답) - String vertexAiGeminiResponse = vertexAiGeminiChatModel.call(message); - - // 응답에서 백틱 및 불필요한 텍스트 제거 - String cleanedResponse = cleanJsonResponse(vertexAiGeminiResponse); - - // json 형식인지 확인 - if (!cleanedResponse.trim().startsWith("{")) { - log.error("Invalid response format: {}", cleanedResponse); - throw new IllegalArgumentException("AI 응답이 JSON 형식이 아닙니다."); - } - - // 2. JSON 데이터를 DTO로 매핑 - ObjectMapper objectMapper = new ObjectMapper(); - AiResponseDTO responseDto = objectMapper.readValue(cleanedResponse, AiResponseDTO.class); - - return ResponseEntity.ok(responseDto); - } catch (IllegalArgumentException e) { - log.error("AI answer failed {}",e.getMessage()); - throw new RuntimeException("AI 응답이 유효하지 않습니다."); - } catch (Exception e) { - log.error("Error while processing AI response: {}", e.getMessage()); - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답 처리 중 문제가 발생했습니다.", e); - } - } - /** - * AI 응답에서 백틱 및 불필요한 텍스트 제거 - */ - private String cleanJsonResponse(String response) { - // 백틱과 "```json" 제거 - String cleaned = response.replaceAll("```json", "").replaceAll("```", "").trim(); - log.debug("Cleaned response: {}", cleaned); - return cleaned; - } - // AI로 비슷한 목격게시물 조회 // 관리자 전부 조회 가능 // 회원 visible + 자신의 글 조회 가능 // 비회원 mapVisible 만 조회 가능 @Operation(summary = "AI로 비슷한 목격게시물을 조회", description = "AI로 실종글타래와 비슷한 목격게시물을 조회합니다.") @GetMapping("/{articleId}") - public ResponseEntity getArticleByArticleIdByAI( + public ResponseEntity> getArticleByArticleIdByAI( @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { String memberRole; @@ -96,14 +60,106 @@ public ResponseEntity getArticleByArticleIdByAI( } log.info("role: {} memberId: {}", memberRole, memberId); - //단견조회, 실종글타래 조회 + //단전조회, 실종글타래 조회 ArticleResponse articleResponse = articleService.getArticleById(articleId, memberRole, memberId); - //목격제보 게시글 전체조회 + //질문 제작 + StringBuilder question = new StringBuilder(); + + // JSON 형식에 대한 예시 설명 + question.append("실종 동물과 유사한 목격 정보를 여러 개 찾습니다. " + + "반드시 JSON 배열 형식으로 응답해주세요. 예시는 다음과 같습니다:\n"); + + question.append("[\n"); + question.append(" {\n"); + question.append(" \"sightArticleId\": 12345,\n"); + question.append(" \"percentage\": 85.52,\n"); + question.append(" \"findSpot\": \"서울특별시 강남구 역삼동\",\n"); + question.append(" \"sightedAt\": \"2024-12-24\",\n"); + question.append(" \"breed\": \"Labrador\",\n"); + question.append(" \"color\": \"Yellow\",\n"); + question.append(" \"gender\": \"Male\"\n"); + question.append(" },\n"); + question.append(" {\n"); + question.append(" \"sightArticleId\": 67890,\n"); + question.append(" \"percentage\": 93.3,\n"); + question.append(" \"findSpot\": \"서울특별시 서초구 서초동\",\n"); + question.append(" \"sightedAt\": \"2024-12-22\",\n"); + question.append(" \"breed\": \"Labrador\",\n"); + question.append(" \"color\": \"Yellow\",\n"); + question.append(" \"gender\": \"Male\"\n"); + question.append(" }\n"); + question.append("]\n\n"); + + question.append("목격 정보는 반드시 `percentage` 내림차순으로 정렬해주세요. " + + "그리고 50% 이상만 응답해 주세요.\n\n"); + + // 실종 동물 정보 + LostResponse lostArticle = (LostResponse) articleResponse; + question.append("실종동물에 대한 설명 : lostArticle.getDescription()"); + question.append("실종 위치: 경도:" + lostArticle.getLongitude() + ", 위도:" + lostArticle.getLatitude()); + question.append("실종 동물 종: " + lostArticle.getBreed()); + question.append("실종 동물 색상: " + lostArticle.getPetColor()); + question.append("실종 동물 성별: " + lostArticle.getPetGender()); + question.append("실종 동물 chipNumber: " + lostArticle.getChipNumber()); + question.append("실종동물 실종된 위치: " + lostArticle.getLostLocation()); + question.append("실종동물 실종일: " + lostArticle.getLostDate()); + + // 여러 목격 게시글 정보 추가 List sightingList = boardService.getAllSightingBoardToList(); - return ResponseEntity.ok(articleResponse); + for (SightingListResponse sightingListResponse : sightingList) { + question.append("목격제보 ID: " + sightingListResponse.getBoardId()); + question.append(", 목격동물 특징: " + sightingListResponse.getContent()); + question.append(", 목격일: " + sightingListResponse.getSightedAt()); + question.append(", 목격위치" + sightingListResponse.getAddress()); + } + + question.append("\n위 정보를 바탕으로 JSON 배열을 만들어 주세요. " + + "각 요소는 SightAIResponse 형식이며, 50% 미만은 제외하고 " + + "percentage 내림차순으로 정렬해 주세요.\n"); + + //AI에게 질문하기 + try { + // 1. Gemini에게 질문 (질문 -> 응답) + String vertexAiGeminiResponse = vertexAiGeminiChatModel.call(question.toString()); + + // 응답에서 백틱 및 불필요한 텍스트 제거 + String cleanedResponse = cleanJsonResponse(vertexAiGeminiResponse); + + // json 형식인지 확인 + if (!cleanedResponse.trim().startsWith("[")) { + log.error("Invalid response format: {}", cleanedResponse); + throw new IllegalArgumentException("AI 응답이 JSON 형식이 아닙니다."); + } + + // 2. JSON 데이터를 DTO로 매핑 버전 + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); //jackson 날짜추가 + + JavaType listType = objectMapper.getTypeFactory() + .constructCollectionType(List.class, SightAIResponse.class); + List answerList = objectMapper.readValue(cleanedResponse, listType); + + return ResponseEntity.ok(answerList); + } catch (IllegalArgumentException e) { + log.error("AI answer failed {}", e.getMessage()); + throw new RuntimeException("AI 응답이 유효하지 않습니다."); + } catch (Exception e) { + log.error("Error while processing AI response: {}", e.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답 처리 중 문제가 발생했습니다.", e); + } } + /** + * AI 응답에서 백틱 및 불필요한 텍스트 제거 + */ + private String cleanJsonResponse(String response) { + // 백틱과 "```json" 제거 + String cleaned = response.replaceAll("```json", "").replaceAll("```", "").trim(); + log.debug("Cleaned response: {}", cleaned); + return cleaned; + } } + diff --git a/src/main/java/com/mallangs/domain/ai/dto/SightAIResponse.java b/src/main/java/com/mallangs/domain/ai/dto/SightAIResponse.java new file mode 100644 index 00000000..f6a41e02 --- /dev/null +++ b/src/main/java/com/mallangs/domain/ai/dto/SightAIResponse.java @@ -0,0 +1,34 @@ +package com.mallangs.domain.ai.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.time.LocalDate; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) // DTO에 정의되지 않은 필드는 무시 +public class SightAIResponse { + + @JsonProperty("sightArticleId") + private Long sightArticleId; + + @JsonProperty("percentage") + private Double percentage; + + @JsonProperty("findSpot") + private String findSpot; + + @JsonProperty("sightedAt") + private LocalDate sightedAt; + + @JsonProperty("breed") + private String breed; + + @JsonProperty("color") + private String color; + + @JsonProperty("gender") + private String gender; + +} diff --git a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java index d9269902..46a75d66 100644 --- a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java +++ b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java @@ -91,6 +91,7 @@ public ResponseEntity getArticleByArticleId( ArticleResponse articleResponse = articleService.getArticleById(articleId, memberRole, memberId); + log.info("articleResponse 결과: {}",articleResponse); return ResponseEntity.ok(articleResponse); } diff --git a/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java b/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java index 6fc0c8de..caedc33b 100644 --- a/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java +++ b/src/main/java/com/mallangs/domain/board/repository/BoardRepository.java @@ -23,7 +23,7 @@ public interface BoardRepository extends JpaRepository { """) Page findAllByBoardType(@Param("boardType") BoardType boardType, Pageable pageable); - //실증글타래 List로 조회 + //목격게시글 List로 조회 List findAllByBoardType(@Param("boardType") BoardType boardType); // 카테고리 이름으로 게시글 조회 From da4a77878d3aacfbc2a7cb3c57ed019c68df63f5 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Wed, 25 Dec 2024 02:14:12 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[feat]=EB=AA=A9=EA=B2=A9=EA=B8=80?= =?UTF-8?q?=ED=83=80=EB=9E=98=20=EB=93=B1=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 3 +- .../dto/request/ArticleCreateRequest.java | 1 + .../request/SightingArticleCreateRequest.java | 1 - .../article/entity/SightingArticle.java | 167 +++++++++--------- .../article/service/ArticleService.java | 9 +- 5 files changed, 91 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java index 46a75d66..c56e0a97 100644 --- a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java +++ b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java @@ -59,8 +59,7 @@ public ResponseEntity createArticle( @Parameter(description = "현재 인증된 사용자 정보", required = true) @AuthenticationPrincipal CustomMemberDetails principal) { Long memberId = principal.getMemberId(); - ArticleResponse articleResponse = articleService.createArticle(articleCreateRequest, - memberId); + ArticleResponse articleResponse = articleService.createArticle(articleCreateRequest,memberId); return ResponseEntity.ok(articleResponse); } diff --git a/src/main/java/com/mallangs/domain/article/dto/request/ArticleCreateRequest.java b/src/main/java/com/mallangs/domain/article/dto/request/ArticleCreateRequest.java index a4c2972a..ea0086c1 100644 --- a/src/main/java/com/mallangs/domain/article/dto/request/ArticleCreateRequest.java +++ b/src/main/java/com/mallangs/domain/article/dto/request/ArticleCreateRequest.java @@ -30,6 +30,7 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = LostCreateRequest.class, name = "lost"), @JsonSubTypes.Type(value = RescueCreateRequest.class, name = "rescue"), + @JsonSubTypes.Type(value = SightingArticleCreateRequest.class, name = "sight"), @JsonSubTypes.Type(value = PlaceCreateRequest.class, name = "place") }) public class ArticleCreateRequest { diff --git a/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java b/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java index abbcab9c..4732df11 100644 --- a/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java +++ b/src/main/java/com/mallangs/domain/article/dto/request/SightingArticleCreateRequest.java @@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.ToString; diff --git a/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java b/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java index e01cd654..31b6a1e0 100644 --- a/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java +++ b/src/main/java/com/mallangs/domain/article/entity/SightingArticle.java @@ -18,91 +18,92 @@ @Entity @Getter @NoArgsConstructor -@DiscriminatorValue("sighting") +@DiscriminatorValue("sight") @SuperBuilder public class SightingArticle extends Article { - @NotNull - private Long lostArticleId; - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private PetType petType; - - private String breed; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private PetGender petGender; - - private String petColor; - - private String chipNumber; - - private LocalDateTime sightDate; - - private String sightLocation; - - @Override - public void applyChanges(Article updatedArticle) { - super.applyChanges(updatedArticle); - - if (updatedArticle instanceof SightingArticle) { - SightingArticle updatedSightingArticle = (SightingArticle) updatedArticle; - if (updatedSightingArticle.getLostArticleId() != null) { - this.lostArticleId = updatedSightingArticle.getLostArticleId(); - } - if (updatedSightingArticle.getPetType() != null) { - this.petType = updatedSightingArticle.getPetType(); - } - if (updatedSightingArticle.getBreed() != null) { - this.breed = updatedSightingArticle.getBreed(); - } - if (updatedSightingArticle.getPetGender() != null) { - this.petGender = updatedSightingArticle.getPetGender(); - } - if (updatedSightingArticle.getPetColor() != null) { - this.petColor = updatedSightingArticle.getPetColor(); - } - if (updatedSightingArticle.getChipNumber() != null) { - this.chipNumber = updatedSightingArticle.getChipNumber(); - } - if (updatedSightingArticle.getSightDate() != null) { - this.sightDate = updatedSightingArticle.getSightDate(); - } - if (updatedSightingArticle.getSightLocation() != null) { - this.sightLocation = updatedSightingArticle.getSightLocation(); - } + @NotNull + private Long lostArticleId; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PetType petType; + + private String breed; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PetGender petGender; + + private String petColor; + + private String chipNumber; + + private LocalDateTime sightDate; + + private String sightLocation; + + @Override + public void applyChanges(Article updatedArticle) { + super.applyChanges(updatedArticle); + + if (updatedArticle instanceof SightingArticle) { + SightingArticle updatedSightingArticle = (SightingArticle) updatedArticle; + if (updatedSightingArticle.getLostArticleId() != null) { + this.lostArticleId = updatedSightingArticle.getLostArticleId(); + } + if (updatedSightingArticle.getPetType() != null) { + this.petType = updatedSightingArticle.getPetType(); + } + if (updatedSightingArticle.getBreed() != null) { + this.breed = updatedSightingArticle.getBreed(); + } + if (updatedSightingArticle.getPetGender() != null) { + this.petGender = updatedSightingArticle.getPetGender(); + } + if (updatedSightingArticle.getPetColor() != null) { + this.petColor = updatedSightingArticle.getPetColor(); + } + if (updatedSightingArticle.getChipNumber() != null) { + this.chipNumber = updatedSightingArticle.getChipNumber(); + } + if (updatedSightingArticle.getSightDate() != null) { + this.sightDate = updatedSightingArticle.getSightDate(); + } + if (updatedSightingArticle.getSightLocation() != null) { + this.sightLocation = updatedSightingArticle.getSightLocation(); + } + } + } + + public static SightingArticle createSightingArticle(Member member, SightingArticleCreateRequest createRequest) { + // GeometryFactory 객체 생성 + GeometryFactory geometryFactory = new GeometryFactory(); + + // 위도와 경도를 기반으로 Coordinate 객체 생성 + Coordinate coordinate = new Coordinate(createRequest.getLongitude(), + createRequest.getLatitude()); + + // Point 객체 생성 + Point geography = geometryFactory.createPoint(coordinate); + geography.setSRID(4326); // SRID 4326 (WGS 84) 설정 + + return SightingArticle.builder() + .lostArticleId(createRequest.getLostArticleId()) + .petType(createRequest.getPetType()) + .breed(createRequest.getBreed()) + .petGender(createRequest.getPetGender()) + .petColor(createRequest.getPetColor()) + .chipNumber(createRequest.getChipNumber()) + .sightDate(createRequest.getSightDate()) + .sightLocation(createRequest.getSightLocation()) + .member(member) + .articleType(createRequest.getArticleType()) + .articleStatus(createRequest.getArticleStatus()) + .title(createRequest.getTitle()) + .geography(geography) + .description(createRequest.getDescription()) + .image(createRequest.getImage()) + .build(); } - } - - public static SightingArticle createSightingArticle(Member member, SightingArticleCreateRequest createRequest) { - // GeometryFactory 객체 생성 - GeometryFactory geometryFactory = new GeometryFactory(); - - // 위도와 경도를 기반으로 Coordinate 객체 생성 - Coordinate coordinate = new Coordinate(createRequest.getLongitude(), - createRequest.getLatitude()); - - // Point 객체 생성 - Point geography = geometryFactory.createPoint(coordinate); - geography.setSRID(4326); // SRID 4326 (WGS 84) 설정 - - return SightingArticle.builder() - .petType(createRequest.getPetType()) - .breed(createRequest.getBreed()) - .petGender(createRequest.getPetGender()) - .petColor(createRequest.getPetColor()) - .chipNumber(createRequest.getChipNumber()) - .sightDate(createRequest.getSightDate()) - .sightLocation(createRequest.getSightLocation()) - .member(member) - .articleType(createRequest.getArticleType()) - .articleStatus(createRequest.getArticleStatus()) - .title(createRequest.getTitle()) - .geography(geography) - .description(createRequest.getDescription()) - .image(createRequest.getImage()) - .build(); - } } diff --git a/src/main/java/com/mallangs/domain/article/service/ArticleService.java b/src/main/java/com/mallangs/domain/article/service/ArticleService.java index ac6d3980..d87cdb52 100644 --- a/src/main/java/com/mallangs/domain/article/service/ArticleService.java +++ b/src/main/java/com/mallangs/domain/article/service/ArticleService.java @@ -36,17 +36,18 @@ public class ArticleService { private final ArticleRepository articleRepository; public ArticleResponse createArticle(ArticleCreateRequest articleCreateRequest, Long memberId) { + //단순 회원조회 Member foundMember = memberRepository.findById(memberId) .orElseThrow(() -> new MallangsCustomException(ErrorCode.MEMBER_NOT_FOUND)); - // 팩토리 매니저를 통해 적절한 팩토리 선택 - log.info(articleCreateRequest.toString()); - log.info(articleCreateRequest.getArticleType().getDescription()); + // 팩토리 매니저를 통해 적절한 팩토리 선택 -> 팩토리의 역할: + log.info("articleCreateRequest: {}",articleCreateRequest.toString()); + log.info("articleCreateRequest 설명: {}",articleCreateRequest.getArticleType().getDescription()); ArticleFactory factory = factoryManager.getFactory( articleCreateRequest.getArticleType().getDescription()); // 팩토리에서 article 생성 - Article article = factory.createArticle(foundMember, articleCreateRequest); + Article article = factory.createArticle(foundMember, articleCreateRequest); article.hideInMap(); // published 상태 아니면 map hidden Article savedArticle = articleRepository.save(article); From fc2704ac3e60233e4e4d57a98db28216141c5e2c Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Wed, 25 Dec 2024 02:20:23 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[feat]=20=EB=AA=A9=EA=B2=A9=EA=B8=80?= =?UTF-8?q?=ED=83=80=EB=9E=98=20=EC=A0=84=EC=B2=B4=EC=A1=B0=ED=9A=8C=20api?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mallangs/domain/article/controller/ArticleController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java index c56e0a97..248e877f 100644 --- a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java +++ b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java @@ -266,5 +266,4 @@ public ResponseEntity> getSightingArticleByLostArticleId( return ResponseEntity.ok(articleResponse); } - } From d7e2ad286eb5b8e64bb72fc8e1d026eb6710f954 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Wed, 25 Dec 2024 02:28:16 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[fix]=20=EC=8B=A4=EC=A2=85=EA=B8=80?= =?UTF-8?q?=ED=83=80=EB=9E=98=20id=EB=A1=9C=20=EB=AA=A9=EA=B2=A9=EA=B8=80?= =?UTF-8?q?=ED=83=80=EB=9E=98=20=EC=A0=84=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?->=20api=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(=ED=86=B5=EC=9D=BC=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/controller/ArticleController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java index 248e877f..aeece841 100644 --- a/src/main/java/com/mallangs/domain/article/controller/ArticleController.java +++ b/src/main/java/com/mallangs/domain/article/controller/ArticleController.java @@ -257,12 +257,12 @@ public ResponseEntity deleteArticle( // 목격글타래 전체조회 @Operation(summary = "목격글타래 전체조회", description = "실종글타래 ID로 관련 목격글타래를 전체 조회합니다.") - @GetMapping("/public/sighting/{articleId}") + @GetMapping("/public/sight/{lostArticleId}") public ResponseEntity> getSightingArticleByLostArticleId( - @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { + @Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long lostArticleId) { //목격제보 글타래 전체조회 - List articleResponse = articleService.getSightArticleByLostId(articleId); + List articleResponse = articleService.getSightArticleByLostId(lostArticleId); return ResponseEntity.ok(articleResponse); } From f174f0685d6549d659c723ac4e40ba53e33a1edd Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Wed, 25 Dec 2024 15:54:06 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[feat]=20develop=20CI/CD=20GCP=20CREDENTI?= =?UTF-8?q?AL=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICDdevelop.yml | 51 ++++++++++++++++--- build.gradle | 2 + .../domain/ai/AIPromptController.java | 1 - .../mallangs/domain/ai/dto/AiResponseDTO.java | 15 ------ 4 files changed, 45 insertions(+), 24 deletions(-) delete mode 100644 src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java diff --git a/.github/workflows/CICDdevelop.yml b/.github/workflows/CICDdevelop.yml index 8a04a187..96618d7d 100644 --- a/.github/workflows/CICDdevelop.yml +++ b/.github/workflows/CICDdevelop.yml @@ -5,38 +5,41 @@ on: branches: - develop - jobs: build: runs-on: ubuntu-latest steps: + # 1. 코드 체크아웃 - uses: actions/checkout@v3 + # 2. Java 설정 - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '17' - # application.yml 파일 생성 + # 3. application.properties 파일 생성 및 설정 - run: touch ./src/main/resources/application.properties - run: echo "${{secrets.APPLICATION}}" > ./src/main/resources/application.properties - run: echo "spring.cloud.aws.credentials.access-key=${{secrets.AWS_ACTION_ACCESS_KEY}}" >> ./src/main/resources/application.properties - run: echo "spring.cloud.aws.credentials.secret-key=${{secrets.AWS_ACTION_SECRET_ACCESS_KEY}}" >> ./src/main/resources/application.properties - - # 생성된 파일들을 아티팩트로 업로드 + # 4. 생성된 파일들을 아티팩트로 업로드 - uses: actions/upload-artifact@v3 with: name: application.properties path: ./src/main/resources/application.properties + # 5. Gradlew 실행 권한 부여 - name: Grant execute permission for gradlew run: chmod +x gradlew + # 6. Gradle 빌드 실행 - name: Build with Gradle run: ./gradlew clean build -x test + # 7. 현재 시간 가져오기 - name: Get current time uses: josStorer/get-current-time@v2 id: current-time @@ -44,13 +47,46 @@ jobs: format: YYYY-MM-DDTHH-mm-ss utcOffset: "+09:00" - # 배포용 패키지 경로 설정 + # 8. 배포용 패키지 경로 설정 - name: Set artifact path run: | artifact=$(ls ./build/libs/*.jar | head -n 1) echo "artifact=$artifact" >> $GITHUB_ENV - # 빈스토크 배포 + # 9. SSH 키 설정 + - name: Set up SSH + uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.EC2_SSH_KEY_TWO }} + # GitHub Secrets에 저장된 EC2 SSH 개인 키를 SSH 에이전트에 로드 + + # 10. GCP Credentials 디코딩 및 EC2로 전송 + - name: Decode GCP Credentials + run: echo "${{ secrets.GCP_CREDENTIALS }}" | base64 --decode > gcp-key.json + # GitHub Secrets에 저장된 Base64 인코딩된 GCP JSON Key를 디코딩하여 로컬에 저장 + + - name: Copy GCP Credentials to EC2 + run: scp -o StrictHostKeyChecking=no gcp-key.json ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP_TWO }}:/home/${{ secrets.EC2_USER }}/gcp-key.json + # 디코딩된 GCP JSON Key 파일을 EC2 서버로 전송 + # YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) + # ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) + + # 11. EC2에서 GCP Credentials 설정 + - name: Set up GCP Credentials on EC2 + run: | + ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP_TWO }} << 'EOF' + mkdir -p ~/.gcp + mv /home/${{ secrets.EC2_USER }}/gcp-key.json ~/.gcp/key.json + chmod 600 ~/.gcp/key.json + export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json + # 환경 변수 설정을 영구적으로 추가 + echo 'export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json' >> ~/.bash_profile + EOF + # EC2 서버에서 GCP Credentials 설정 + # YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) + # ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) + + # 12. Elastic Beanstalk에 배포 - name: Deploy to Elastic Beanstalk uses: einaregilsson/beanstalk-deploy@v20 with: @@ -60,5 +96,4 @@ jobs: environment_name: Mallangs2-two-env version_label: github-action-${{ steps.current-time.outputs.time }} region: ap-northeast-2 - deployment_package: ${{ env.artifact }} - + deployment_package: ${{ env.artifact }} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8cf4a149..9c00fc83 100644 --- a/build.gradle +++ b/build.gradle @@ -108,6 +108,8 @@ dependencies { //AI prompt implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter:1.0.0-SNAPSHOT' + + //jackson 날짜변환 위한 의존성 implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } diff --git a/src/main/java/com/mallangs/domain/ai/AIPromptController.java b/src/main/java/com/mallangs/domain/ai/AIPromptController.java index b8536e80..81c8cba1 100644 --- a/src/main/java/com/mallangs/domain/ai/AIPromptController.java +++ b/src/main/java/com/mallangs/domain/ai/AIPromptController.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.mallangs.domain.ai.dto.AiResponseDTO; import com.mallangs.domain.ai.dto.SightAIResponse; import com.mallangs.domain.article.dto.response.ArticleResponse; import com.mallangs.domain.article.dto.response.LostResponse; diff --git a/src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java b/src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java deleted file mode 100644 index e8a564da..00000000 --- a/src/main/java/com/mallangs/domain/ai/dto/AiResponseDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.mallangs.domain.ai.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -@Data -@JsonIgnoreProperties(ignoreUnknown = true) // DTO에 정의되지 않은 필드는 무시 -public class AiResponseDTO { - @JsonProperty("response") - private String response; - - @JsonProperty("details") - private String details; -} From 335a2f7bf71efaa34b2e3ff86151470d5325ff29 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Wed, 25 Dec 2024 16:39:57 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[fix]=20develop=20CI/CD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICDdevelop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICDdevelop.yml b/.github/workflows/CICDdevelop.yml index 96618d7d..cc377477 100644 --- a/.github/workflows/CICDdevelop.yml +++ b/.github/workflows/CICDdevelop.yml @@ -21,7 +21,7 @@ jobs: # 3. application.properties 파일 생성 및 설정 - run: touch ./src/main/resources/application.properties - - run: echo "${{secrets.APPLICATION}}" > ./src/main/resources/application.properties + - run: echo "${{secrets.APPLICATION2}}" > ./src/main/resources/application.properties - run: echo "spring.cloud.aws.credentials.access-key=${{secrets.AWS_ACTION_ACCESS_KEY}}" >> ./src/main/resources/application.properties - run: echo "spring.cloud.aws.credentials.secret-key=${{secrets.AWS_ACTION_SECRET_ACCESS_KEY}}" >> ./src/main/resources/application.properties From 4a3453820745bb13e536a50a9053a9aeee6b2d04 Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Thu, 26 Dec 2024 13:01:18 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=EA=B9=83=20=EC=95=A1=EC=85=98=20v4?= =?UTF-8?q?=EB=A1=9C=20upload=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICDdevelop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICDdevelop.yml b/.github/workflows/CICDdevelop.yml index cc377477..a4a47fa5 100644 --- a/.github/workflows/CICDdevelop.yml +++ b/.github/workflows/CICDdevelop.yml @@ -26,7 +26,7 @@ jobs: - run: echo "spring.cloud.aws.credentials.secret-key=${{secrets.AWS_ACTION_SECRET_ACCESS_KEY}}" >> ./src/main/resources/application.properties # 4. 생성된 파일들을 아티팩트로 업로드 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: application.properties path: ./src/main/resources/application.properties From dc7b496e971b4e890b979e749087249aaf720c3d Mon Sep 17 00:00:00 2001 From: Dom1046 Date: Thu, 26 Dec 2024 16:11:53 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[feat]=20main=20branch=20CI/CD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 2232e29e..0d86d231 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -26,7 +26,7 @@ jobs: # 생성된 파일들을 아티팩트로 업로드 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: application.properties path: ./src/main/resources/application.properties @@ -50,6 +50,39 @@ jobs: artifact=$(ls ./build/libs/*.jar | head -n 1) echo "artifact=$artifact" >> $GITHUB_ENV + # 9. SSH 키 설정 + - name: Set up SSH + uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.EC2_SSH_KEY }} + # GitHub Secrets에 저장된 EC2 SSH 개인 키를 SSH 에이전트에 로드 + + # 10. GCP Credentials 디코딩 및 EC2로 전송 + - name: Decode GCP Credentials + run: echo "${{ secrets.GCP_CREDENTIALS }}" | base64 --decode > gcp-key.json + # GitHub Secrets에 저장된 Base64 인코딩된 GCP JSON Key를 디코딩하여 로컬에 저장 + + - name: Copy GCP Credentials to EC2 + run: scp -o StrictHostKeyChecking=no gcp-key.json ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP }}:/home/${{ secrets.EC2_USER }}/gcp-key.json + # 디코딩된 GCP JSON Key 파일을 EC2 서버로 전송 + # YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) + # ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) + + # 11. EC2에서 GCP Credentials 설정 + - name: Set up GCP Credentials on EC2 + run: | + ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP }} << 'EOF' + mkdir -p ~/.gcp + mv /home/${{ secrets.EC2_USER }}/gcp-key.json ~/.gcp/key.json + chmod 600 ~/.gcp/key.json + export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json + # 환경 변수 설정을 영구적으로 추가 + echo 'export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json' >> ~/.bash_profile + EOF + # EC2 서버에서 GCP Credentials 설정 + # YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) + # ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) + # 빈스토크 배포 - name: Deploy to Elastic Beanstalk uses: einaregilsson/beanstalk-deploy@v20