diff --git a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java index 92f8296..815a1b2 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java +++ b/src/main/java/com/hyetaekon/hyetaekon/UserInterest/entity/UserInterestEnum.java @@ -13,6 +13,8 @@ public enum UserInterestEnum { HEALTH_MEDICAL("보건·의료", "관심주제"), CULTURE_ENVIRONMENT("문화·환경", "관심주제"), LIFE_STABILITY("생활안정", "관심주제"), + PROTECTION_CARE("보호·돌봄", "관심주제"), // 새로 추가된 카테고리 + OTHER("기타", "관심주제"), // SpecialGroup 관련 관심사 IS_MULTI_CULTURAL("다문화가족", "가구형태"), diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/controller/AnswerController.java b/src/main/java/com/hyetaekon/hyetaekon/answer/controller/AnswerController.java index fbf36bd..3417bdd 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/controller/AnswerController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/controller/AnswerController.java @@ -1,4 +1,42 @@ package com.hyetaekon.hyetaekon.answer.controller; +import com.hyetaekon.hyetaekon.answer.dto.AnswerDto; +import com.hyetaekon.hyetaekon.answer.service.AnswerService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/posts/{postId}/answers") +@RequiredArgsConstructor public class AnswerController { + + private final AnswerService answerService; + + // 답변 작성 + @PostMapping + @PreAuthorize("hasRole('USER')") + public ResponseEntity createAnswer(@PathVariable Long postId, @RequestBody AnswerDto answerDto) { + AnswerDto createdAnswer = answerService.createAnswer(postId, answerDto); + return ResponseEntity.status(HttpStatus.CREATED).body(createdAnswer); + } + + // 답변 채택 + @PutMapping("/{answerId}/select") + @PreAuthorize("hasRole('USER')") + public ResponseEntity selectAnswer(@PathVariable Long answerId) { + answerService.selectAnswer(answerId); + return ResponseEntity.ok().build(); + } + + // 답변 삭제 (관리자만 가능) + @DeleteMapping("/admin/answers/{answerId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteAnswer(@PathVariable Long answerId) { + answerService.deleteAnswer(answerId); + return ResponseEntity.noContent().build(); + } } + diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/dto/AnswerDto.java b/src/main/java/com/hyetaekon/hyetaekon/answer/dto/AnswerDto.java index e970a89..d3bd5ce 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/dto/AnswerDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/dto/AnswerDto.java @@ -1,4 +1,14 @@ package com.hyetaekon.hyetaekon.answer.dto; +import lombok.Data; +import java.time.LocalDateTime; + +@Data public class AnswerDto { + private Long id; + private Long postId; + private Long userId; + private String content; + private LocalDateTime createdAt; + private boolean selected; } diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java b/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java index b0e58f3..a54cbcd 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java @@ -1,4 +1,33 @@ package com.hyetaekon.hyetaekon.answer.entity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "answer") public class Answer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 답변 ID + + @Column(name = "post_id", nullable = false) + private Long postId; // 게시글 ID + + @Column(name = "user_id", nullable = false) + private Long userId; // 회원 ID + + @Column(name = "content", columnDefinition = "TEXT", nullable = false) + private String content; // 답변 내용 + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; // 생성일 + + @Column(name = "selected", nullable = false) + private boolean selected; // 채택 여부 } diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/mapper/AnswerMapper.java b/src/main/java/com/hyetaekon/hyetaekon/answer/mapper/AnswerMapper.java index 2cd41cb..5b87c17 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/mapper/AnswerMapper.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/mapper/AnswerMapper.java @@ -1,4 +1,19 @@ package com.hyetaekon.hyetaekon.answer.mapper; +import com.hyetaekon.hyetaekon.answer.dto.AnswerDto; +import com.hyetaekon.hyetaekon.answer.entity.Answer; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") public interface AnswerMapper { + AnswerMapper INSTANCE = Mappers.getMapper(AnswerMapper.class); + + AnswerDto toDto(Answer answer); + + @Mapping(target = "id", ignore = true) // id는 자동 생성되므로 무시 + @Mapping(target = "selected", ignore = true) // 기본값 false 처리 + @Mapping(target = "createdAt", ignore = true) // createdAt 자동 설정 + Answer toEntity(AnswerDto answerDto); } diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/repository/AnswerRepository.java b/src/main/java/com/hyetaekon/hyetaekon/answer/repository/AnswerRepository.java index 2cdc096..6a587aa 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/repository/AnswerRepository.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/repository/AnswerRepository.java @@ -1,4 +1,9 @@ package com.hyetaekon.hyetaekon.answer.repository; -public class AnswerRepository { +import com.hyetaekon.hyetaekon.answer.entity.Answer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AnswerRepository extends JpaRepository { } diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/service/AnswerService.java b/src/main/java/com/hyetaekon/hyetaekon/answer/service/AnswerService.java index eefb31f..04a5eaa 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/service/AnswerService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/service/AnswerService.java @@ -1,4 +1,34 @@ package com.hyetaekon.hyetaekon.answer.service; +import com.hyetaekon.hyetaekon.answer.dto.AnswerDto; +import com.hyetaekon.hyetaekon.answer.entity.Answer; +import com.hyetaekon.hyetaekon.answer.mapper.AnswerMapper; +import com.hyetaekon.hyetaekon.answer.repository.AnswerRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor public class AnswerService { + private final AnswerRepository answerRepository; + private final AnswerMapper answerMapper; + + public AnswerDto createAnswer(Long postId, AnswerDto answerDto) { + Answer answer = answerMapper.toEntity(answerDto); + answer.setPostId(postId); + answer = answerRepository.save(answer); + return answerMapper.toDto(answer); + } + + public void selectAnswer(Long answerId) { + Answer answer = answerRepository.findById(answerId) + .orElseThrow(() -> new EntityNotFoundException("Answer not found")); + answer.setSelected(true); + answerRepository.save(answer); + } + + public void deleteAnswer(Long answerId) { + answerRepository.deleteById(answerId); + } } diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/controller/AnswerController.java b/src/main/java/com/hyetaekon/hyetaekon/banner/controller/AnswerController.java deleted file mode 100644 index 55ed580..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/banner/controller/AnswerController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.banner.controller; - -public class AnswerController { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/controller/BannerController.java b/src/main/java/com/hyetaekon/hyetaekon/banner/controller/BannerController.java new file mode 100644 index 0000000..89b9a1e --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/banner/controller/BannerController.java @@ -0,0 +1,67 @@ +package com.hyetaekon.hyetaekon.banner.controller; + +import com.hyetaekon.hyetaekon.banner.dto.BannerDto; +import com.hyetaekon.hyetaekon.banner.service.BannerService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/banners") +@RequiredArgsConstructor +public class BannerController { + private final BannerService bannerService; + + // 배너 목록 조회 (ALL) + @GetMapping + public ResponseEntity> getBanners() { + return ResponseEntity.ok(bannerService.getBanners()); + } + + // 배너 상세 조회 (ALL) + @GetMapping("/{bannerId}") + public ResponseEntity getBanner(@PathVariable Long bannerId) { + return ResponseEntity.ok(bannerService.getBanner(bannerId)); + } + + // 배너 목록 조회 (ADMIN) + @GetMapping("/admin") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getAdminBanners() { + return ResponseEntity.ok(bannerService.getAdminBanners()); + } + + // 배너 등록 (ADMIN) + @PostMapping("/admin") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity createBanner(@RequestBody BannerDto bannerDto) { + return ResponseEntity.ok(bannerService.createBanner(bannerDto)); + } + + // 배너 수정 (ADMIN) + @PutMapping("/admin/{bannerId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity updateBanner( + @PathVariable Long bannerId, @RequestBody BannerDto bannerDto) { + return ResponseEntity.ok(bannerService.updateBanner(bannerId, bannerDto)); + } + + // 배너 삭제 (ADMIN) + @DeleteMapping("/admin/{bannerId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteBanner(@PathVariable Long bannerId) { + bannerService.deleteBanner(bannerId); + return ResponseEntity.noContent().build(); + } + + // 배너 순서 변경 (ADMIN) + @PatchMapping("/admin") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity updateBannerOrder(@RequestBody List bannerIds) { + bannerService.updateBannerOrder(bannerIds); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/dto/AnswerDto.java b/src/main/java/com/hyetaekon/hyetaekon/banner/dto/AnswerDto.java deleted file mode 100644 index fafc17d..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/banner/dto/AnswerDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.banner.dto; - -public class AnswerDto { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/dto/BannerDto.java b/src/main/java/com/hyetaekon/hyetaekon/banner/dto/BannerDto.java new file mode 100644 index 0000000..3578b12 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/banner/dto/BannerDto.java @@ -0,0 +1,14 @@ +package com.hyetaekon.hyetaekon.banner.dto; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class BannerDto { + private Long id; + private String title; + private String imageUrl; + private String linkUrl; + private int displayOrder; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/entity/Answer.java b/src/main/java/com/hyetaekon/hyetaekon/banner/entity/Answer.java deleted file mode 100644 index fe64651..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/banner/entity/Answer.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.banner.entity; - -public class Answer { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/entity/Banner.java b/src/main/java/com/hyetaekon/hyetaekon/banner/entity/Banner.java new file mode 100644 index 0000000..f9b4852 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/banner/entity/Banner.java @@ -0,0 +1,36 @@ +package com.hyetaekon.hyetaekon.banner.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "banner") +public class Banner { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 배너 ID + + @Column(name = "title", nullable = false, length = 100) + private String title; // 배너 제목 + + @Column(name = "image_url", nullable = false, length = 500) + private String imageUrl; // 배너 이미지 URL + + @Column(name = "link_url", length = 500) + private String linkUrl; // 배너 클릭 시 이동할 URL + + @Column(name = "display_order", nullable = false) + private int displayOrder; // 배너 정렬 순서 + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; // 생성일 + + @Column(name = "updated_at") + private LocalDateTime updatedAt; // 수정일 +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/mapper/AnswerMapper.java b/src/main/java/com/hyetaekon/hyetaekon/banner/mapper/AnswerMapper.java deleted file mode 100644 index 2cd9b2c..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/banner/mapper/AnswerMapper.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.banner.mapper; - -public interface AnswerMapper { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/mapper/BannerMapper.java b/src/main/java/com/hyetaekon/hyetaekon/banner/mapper/BannerMapper.java new file mode 100644 index 0000000..526e7a9 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/banner/mapper/BannerMapper.java @@ -0,0 +1,17 @@ +package com.hyetaekon.hyetaekon.banner.mapper; + +import com.hyetaekon.hyetaekon.banner.dto.BannerDto; +import com.hyetaekon.hyetaekon.banner.entity.Banner; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") +public interface BannerMapper { + BannerMapper INSTANCE = Mappers.getMapper(BannerMapper.class); + + BannerDto toDto(Banner banner); + + @Mapping(target = "id", ignore = true) // id는 자동 생성됨 + Banner toEntity(BannerDto bannerDto); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/repository/AnswerRepository.java b/src/main/java/com/hyetaekon/hyetaekon/banner/repository/AnswerRepository.java deleted file mode 100644 index 49299aa..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/banner/repository/AnswerRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.banner.repository; - -public class AnswerRepository { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/repository/BannerRepository.java b/src/main/java/com/hyetaekon/hyetaekon/banner/repository/BannerRepository.java new file mode 100644 index 0000000..c5174d9 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/banner/repository/BannerRepository.java @@ -0,0 +1,9 @@ +package com.hyetaekon.hyetaekon.banner.repository; + +import com.hyetaekon.hyetaekon.banner.entity.Banner; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface BannerRepository extends JpaRepository { + List findAllByOrderByDisplayOrderAsc(); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/service/AnswerService.java b/src/main/java/com/hyetaekon/hyetaekon/banner/service/AnswerService.java deleted file mode 100644 index fc63b81..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/banner/service/AnswerService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.banner.service; - -public class AnswerService { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/banner/service/BannerService.java b/src/main/java/com/hyetaekon/hyetaekon/banner/service/BannerService.java new file mode 100644 index 0000000..29c2bb9 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/banner/service/BannerService.java @@ -0,0 +1,74 @@ +package com.hyetaekon.hyetaekon.banner.service; + +import com.hyetaekon.hyetaekon.banner.dto.BannerDto; +import com.hyetaekon.hyetaekon.banner.entity.Banner; +import com.hyetaekon.hyetaekon.banner.mapper.BannerMapper; +import com.hyetaekon.hyetaekon.banner.repository.BannerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BannerService { + private final BannerRepository bannerRepository; + + // 배너 목록 조회 (모든 사용자) + public List getBanners() { + List banners = bannerRepository.findAllByOrderByDisplayOrderAsc(); + return banners.stream().map(BannerMapper.INSTANCE::toDto).collect(Collectors.toList()); + } + + // 배너 상세 조회 (모든 사용자) + public BannerDto getBanner(Long bannerId) { + Banner banner = bannerRepository.findById(bannerId) + .orElseThrow(() -> new IllegalArgumentException("해당 배너가 존재하지 않습니다.")); + return BannerMapper.INSTANCE.toDto(banner); + } + + // 배너 목록 조회 (관리자) + public List getAdminBanners() { + return getBanners(); + } + + // 배너 등록 (관리자) + public BannerDto createBanner(BannerDto bannerDto) { + Banner banner = BannerMapper.INSTANCE.toEntity(bannerDto); + banner.setCreatedAt(LocalDateTime.now()); + banner.setUpdatedAt(LocalDateTime.now()); + return BannerMapper.INSTANCE.toDto(bannerRepository.save(banner)); + } + + // 배너 수정 (관리자) + public BannerDto updateBanner(Long bannerId, BannerDto bannerDto) { + Banner banner = bannerRepository.findById(bannerId) + .orElseThrow(() -> new IllegalArgumentException("해당 배너가 존재하지 않습니다.")); + + banner.setTitle(bannerDto.getTitle()); + banner.setImageUrl(bannerDto.getImageUrl()); + banner.setLinkUrl(bannerDto.getLinkUrl()); + banner.setDisplayOrder(bannerDto.getDisplayOrder()); + banner.setUpdatedAt(LocalDateTime.now()); + + return BannerMapper.INSTANCE.toDto(bannerRepository.save(banner)); + } + + // 배너 삭제 (관리자) + public void deleteBanner(Long bannerId) { + bannerRepository.deleteById(bannerId); + } + + // 배너 순서 변경 (관리자) + @Transactional + public void updateBannerOrder(List bannerIds) { + int order = 1; + for (Long bannerId : bannerIds) { + Banner banner = bannerRepository.findById(bannerId) + .orElseThrow(() -> new IllegalArgumentException("배너를 찾을 수 없습니다.")); + banner.setDisplayOrder(order++); + } + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/bookmark/controller/BookMarkController.java b/src/main/java/com/hyetaekon/hyetaekon/bookmark/controller/BookMarkController.java index cee5f98..e7b6c34 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/bookmark/controller/BookMarkController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/bookmark/controller/BookMarkController.java @@ -19,7 +19,7 @@ public class BookMarkController { // 북마크 추가 @PostMapping public ResponseEntity addBookmark( - @PathVariable("serviceId") Long serviceId, + @PathVariable("serviceId") String serviceId, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { bookmarkService.addBookmark(serviceId, customUserDetails.getId()); @@ -29,7 +29,7 @@ public ResponseEntity addBookmark( // 북마크 제거 @DeleteMapping public ResponseEntity removeBookmark( - @PathVariable("serviceId") Long serviceId, + @PathVariable("serviceId") String serviceId, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { bookmarkService.removeBookmark(serviceId, customUserDetails.getId()); diff --git a/src/main/java/com/hyetaekon/hyetaekon/bookmark/repository/BookmarkRepository.java b/src/main/java/com/hyetaekon/hyetaekon/bookmark/repository/BookmarkRepository.java index f43e20b..790f102 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/bookmark/repository/BookmarkRepository.java +++ b/src/main/java/com/hyetaekon/hyetaekon/bookmark/repository/BookmarkRepository.java @@ -11,9 +11,9 @@ @Repository public interface BookmarkRepository extends JpaRepository { - boolean existsByUserIdAndPublicServiceId(Long userId, Long serviceId); + boolean existsByUserIdAndPublicServiceId(Long userId, String serviceId); - Optional findByUserIdAndPublicServiceId(Long userId, Long serviceId); + Optional findByUserIdAndPublicServiceId(Long userId, String serviceId); /*@Query("SELECT b FROM Bookmark b " + "JOIN FETCH b.publicService ps " + diff --git a/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java b/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java index 41aaad6..fb9d821 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java @@ -21,7 +21,7 @@ public class BookmarkService { private final UserRepository userRepository; private final PublicServiceRepository publicServiceRepository; - public void addBookmark(Long serviceId, Long userId) { + public void addBookmark(String serviceId, Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new GlobalException(BOOKMARK_USER_NOT_FOUND)); @@ -45,7 +45,7 @@ public void addBookmark(Long serviceId, Long userId) { } @Transactional - public void removeBookmark(Long serviceId, Long userId) { + public void removeBookmark(String serviceId, Long userId) { Bookmark bookmark = bookmarkRepository.findByUserIdAndPublicServiceId(userId, serviceId) .orElseThrow(() -> new GlobalException(BOOKMARK_NOT_FOUND)); diff --git a/src/main/java/com/hyetaekon/hyetaekon/chatbot/controller/ChatbotController.java b/src/main/java/com/hyetaekon/hyetaekon/chatbot/controller/ChatbotController.java new file mode 100644 index 0000000..e9f9384 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/chatbot/controller/ChatbotController.java @@ -0,0 +1,26 @@ +package com.hyetaekon.hyetaekon.chatbot.controller; + +import com.hyetaekon.hyetaekon.chatbot.dto.ChatbotDto; +import com.hyetaekon.hyetaekon.chatbot.service.ChatbotService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/chatbot") +@RequiredArgsConstructor +public class ChatbotController { + private final ChatbotService chatbotService; + + // 📌 사용자가 질문하면 답변을 반환하는 API + @GetMapping + public ResponseEntity getAnswer(@RequestParam String question) { + return ResponseEntity.ok(chatbotService.getAnswer(question)); + } + + // 📌 새로운 질문-답변을 DB에 추가하는 API (관리자용) + @PostMapping("/add") + public ResponseEntity addQuestionAndAnswer(@RequestBody ChatbotDto chatbotDto) { + return ResponseEntity.ok(chatbotService.addQuestionAndAnswer(chatbotDto)); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/chatbot/dto/ChatbotDto.java b/src/main/java/com/hyetaekon/hyetaekon/chatbot/dto/ChatbotDto.java new file mode 100644 index 0000000..929acdc --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/chatbot/dto/ChatbotDto.java @@ -0,0 +1,13 @@ +package com.hyetaekon.hyetaekon.chatbot.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChatbotDto { + private String question; + private String answer; +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/chatbot/entity/Chatbot.java b/src/main/java/com/hyetaekon/hyetaekon/chatbot/entity/Chatbot.java new file mode 100644 index 0000000..dcce1e0 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/chatbot/entity/Chatbot.java @@ -0,0 +1,28 @@ +package com.hyetaekon.hyetaekon.chatbot.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class Chatbot { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 500) + private String question; // 질문 + + @Column(nullable = false, length = 1000) + private String answer; // 답변 + + public Chatbot(String question, String answer) { + this.question = question; + this.answer = answer; + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/chatbot/mapper/ChatbotMapper.java b/src/main/java/com/hyetaekon/hyetaekon/chatbot/mapper/ChatbotMapper.java new file mode 100644 index 0000000..17c9ca2 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/chatbot/mapper/ChatbotMapper.java @@ -0,0 +1,14 @@ +package com.hyetaekon.hyetaekon.chatbot.mapper; + +import com.hyetaekon.hyetaekon.chatbot.dto.ChatbotDto; +import com.hyetaekon.hyetaekon.chatbot.entity.Chatbot; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ChatbotMapper { + ChatbotMapper INSTANCE = Mappers.getMapper(ChatbotMapper.class); + + ChatbotDto toDto(Chatbot chatbot); + Chatbot toEntity(ChatbotDto chatbotDto); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/chatbot/repository/ChatbotRepository.java b/src/main/java/com/hyetaekon/hyetaekon/chatbot/repository/ChatbotRepository.java new file mode 100644 index 0000000..c230015 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/chatbot/repository/ChatbotRepository.java @@ -0,0 +1,10 @@ +package com.hyetaekon.hyetaekon.chatbot.repository; + +import com.hyetaekon.hyetaekon.chatbot.entity.Chatbot; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ChatbotRepository extends JpaRepository { + Optional findByQuestion(String question); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/chatbot/service/ChatbotService.java b/src/main/java/com/hyetaekon/hyetaekon/chatbot/service/ChatbotService.java new file mode 100644 index 0000000..af93923 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/chatbot/service/ChatbotService.java @@ -0,0 +1,36 @@ +package com.hyetaekon.hyetaekon.chatbot.service; + +import com.hyetaekon.hyetaekon.chatbot.dto.ChatbotDto; +import com.hyetaekon.hyetaekon.chatbot.entity.Chatbot; +import com.hyetaekon.hyetaekon.chatbot.mapper.ChatbotMapper; +import com.hyetaekon.hyetaekon.chatbot.repository.ChatbotRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ChatbotService { + private final ChatbotRepository chatbotRepository; + + // 📌 질문을 DB에서 찾아서 답변을 반환하는 메서드 + public ChatbotDto getAnswer(String question) { + Optional chatbot = chatbotRepository.findByQuestion(question); + + // 📌 질문이 DB에 있다면 해당 답변 반환 + if (chatbot.isPresent()) { + return ChatbotMapper.INSTANCE.toDto(chatbot.get()); + } + + // 📌 질문이 DB에 없을 경우 기본 응답 반환 + return new ChatbotDto(question, "죄송해요, 해당 질문에 대한 답변을 찾을 수 없어요."); + } + + // 📌 새로운 질문-답변을 DB에 추가하는 메서드 + public ChatbotDto addQuestionAndAnswer(ChatbotDto chatbotDto) { + Chatbot chatbot = ChatbotMapper.INSTANCE.toEntity(chatbotDto); + chatbotRepository.save(chatbot); + return chatbotDto; + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/controller/AnswerController.java b/src/main/java/com/hyetaekon/hyetaekon/comment/controller/AnswerController.java deleted file mode 100644 index b5ee124..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/controller/AnswerController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.comment.controller; - -public class AnswerController { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/controller/CommentController.java b/src/main/java/com/hyetaekon/hyetaekon/comment/controller/CommentController.java new file mode 100644 index 0000000..c0c314a --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/controller/CommentController.java @@ -0,0 +1,65 @@ +package com.hyetaekon.hyetaekon.comment.controller; + +import com.hyetaekon.hyetaekon.comment.dto.CommentDto; +import com.hyetaekon.hyetaekon.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/posts/{postId}/comments") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + // 게시글 댓글 목록 조회 (페이징 지원) + @GetMapping + @PreAuthorize("hasRole('USER')") + public ResponseEntity> getComments(@PathVariable Long postId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + Page comments = commentService.getComments(postId, page, size); + return ResponseEntity.ok(comments); + } + + // 게시글에 댓글 작성 + @PostMapping + @PreAuthorize("hasRole('USER')") + public ResponseEntity createComment(@PathVariable Long postId, @RequestBody CommentDto commentDto) { + CommentDto createdComment = commentService.createComment(postId, commentDto); + return ResponseEntity.status(HttpStatus.CREATED).body(createdComment); + } + + // 대댓글 목록 조회 + @GetMapping("/{commentId}/replies") + @PreAuthorize("hasRole('USER')") + public ResponseEntity> getReplies(@PathVariable Long postId, + @PathVariable Long commentId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "5") int size) { + Page replies = commentService.getReplies(postId, commentId, page, size); + return ResponseEntity.ok(replies); + } + + // 대댓글 작성 + @PostMapping("/{commentId}/replies") + @PreAuthorize("hasRole('USER')") + public ResponseEntity createReply(@PathVariable Long postId, + @PathVariable Long commentId, + @RequestBody CommentDto commentDto) { + CommentDto createdReply = commentService.createReply(postId, commentId, commentDto); + return ResponseEntity.status(HttpStatus.CREATED).body(createdReply); + } + + // 댓글 삭제 (관리자만 가능) + @DeleteMapping("/admin/comments/{commentId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteComment(@PathVariable Long commentId) { + commentService.deleteComment(commentId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/dto/AnswerDto.java b/src/main/java/com/hyetaekon/hyetaekon/comment/dto/AnswerDto.java deleted file mode 100644 index 84aa543..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/dto/AnswerDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.comment.dto; - -public class AnswerDto { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/dto/CommentDto.java b/src/main/java/com/hyetaekon/hyetaekon/comment/dto/CommentDto.java new file mode 100644 index 0000000..eed3798 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/dto/CommentDto.java @@ -0,0 +1,18 @@ +package com.hyetaekon.hyetaekon.comment.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CommentDto { + private Long id; + private Long postId; + private Long parentId; // 대댓글일 경우 부모 댓글 ID + private String content; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Answer.java b/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Answer.java deleted file mode 100644 index 94dcb73..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Answer.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.comment.entity; - -public class Answer { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java b/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java new file mode 100644 index 0000000..bf9281e --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java @@ -0,0 +1,30 @@ +package com.hyetaekon.hyetaekon.comment.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long postId; + + private Long parentId; // 대댓글이면 부모 댓글 ID, 아니면 null + + @Column(nullable = false, length = 1000) + private String content; + + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/mapper/AnswerMapper.java b/src/main/java/com/hyetaekon/hyetaekon/comment/mapper/AnswerMapper.java deleted file mode 100644 index df25d0c..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/mapper/AnswerMapper.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.comment.mapper; - -public interface AnswerMapper { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/mapper/CommentMapper.java b/src/main/java/com/hyetaekon/hyetaekon/comment/mapper/CommentMapper.java new file mode 100644 index 0000000..61fa050 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/mapper/CommentMapper.java @@ -0,0 +1,18 @@ +package com.hyetaekon.hyetaekon.comment.mapper; + +import com.hyetaekon.hyetaekon.comment.dto.CommentDto; +import com.hyetaekon.hyetaekon.comment.entity.Comment; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + CommentMapper INSTANCE = Mappers.getMapper(CommentMapper.class); + + CommentDto toDto(Comment comment); + + @Mapping(target = "id", ignore = true) // id는 자동 생성되므로 무시 + @Mapping(target = "createdAt", ignore = true) // createdAt 자동 설정 + Comment toEntity(CommentDto commentDto); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/repository/AnswerRepository.java b/src/main/java/com/hyetaekon/hyetaekon/comment/repository/AnswerRepository.java deleted file mode 100644 index b485726..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/repository/AnswerRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.comment.repository; - -public class AnswerRepository { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/repository/CommentRepository.java b/src/main/java/com/hyetaekon/hyetaekon/comment/repository/CommentRepository.java new file mode 100644 index 0000000..f31d589 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/repository/CommentRepository.java @@ -0,0 +1,13 @@ +package com.hyetaekon.hyetaekon.comment.repository; + +import com.hyetaekon.hyetaekon.comment.entity.Comment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { + Page findByPostId(Long postId, Pageable pageable); + Page findByPostIdAndParentId(Long postId, Long parentId, Pageable pageable); +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/service/AnswerService.java b/src/main/java/com/hyetaekon/hyetaekon/comment/service/AnswerService.java deleted file mode 100644 index 7535744..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/service/AnswerService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.comment.service; - -public class AnswerService { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/service/CommentService.java b/src/main/java/com/hyetaekon/hyetaekon/comment/service/CommentService.java new file mode 100644 index 0000000..3790730 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/service/CommentService.java @@ -0,0 +1,49 @@ +package com.hyetaekon.hyetaekon.comment.service; + +import com.hyetaekon.hyetaekon.comment.dto.CommentDto; +import com.hyetaekon.hyetaekon.comment.entity.Comment; +import com.hyetaekon.hyetaekon.comment.mapper.CommentMapper; +import com.hyetaekon.hyetaekon.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CommentService { + private final CommentRepository commentRepository; + private final CommentMapper commentMapper; + + public Page getComments(Long postId, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return commentRepository.findByPostId(postId, pageable) + .map(commentMapper::toDto); + } + + public CommentDto createComment(Long postId, CommentDto commentDto) { + Comment comment = commentMapper.toEntity(commentDto); + comment.setPostId(postId); + comment = commentRepository.save(comment); + return commentMapper.toDto(comment); + } + + public Page getReplies(Long postId, Long commentId, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return commentRepository.findByPostIdAndParentId(postId, commentId, pageable) + .map(commentMapper::toDto); + } + + public CommentDto createReply(Long postId, Long commentId, CommentDto commentDto) { + Comment reply = commentMapper.toEntity(commentDto); + reply.setPostId(postId); + reply.setParentId(commentId); + reply = commentRepository.save(reply); + return commentMapper.toDto(reply); + } + + public void deleteComment(Long commentId) { + commentRepository.deleteById(commentId); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/converter/ServiceCategoryConverter.java b/src/main/java/com/hyetaekon/hyetaekon/common/converter/ServiceCategoryConverter.java index 1ec1b35..ed9915f 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/converter/ServiceCategoryConverter.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/converter/ServiceCategoryConverter.java @@ -6,7 +6,9 @@ import com.hyetaekon.hyetaekon.publicservice.entity.ServiceCategory; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Converter(autoApply = true) // autoApply를 true로 설정하면 @Convert없이 해당 타입에 대해 자동으로 변환 public class ServiceCategoryConverter implements AttributeConverter { @@ -17,12 +19,20 @@ public String convertToDatabaseColumn(ServiceCategory serviceCategory) { @Override public ServiceCategory convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + for (ServiceCategory serviceCategory : ServiceCategory.values()) { if (serviceCategory.getType().equals(dbData)) { return serviceCategory; } } - throw new GlobalException(ErrorCode.SERVICE_CATEGORY_NOT_FOUND); + // throw new GlobalException(ErrorCode.SERVICE_CATEGORY_NOT_FOUND); + + // 일치하는 카테고리가 없을 경우 로그를 남기고 기본값 반환 + log.warn("Unknown service category found: '{}'. Using 'OTHER' category instead.", dbData); + return ServiceCategory.OTHER; } } diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceConditionsDataDto.java b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceConditionsDataDto.java index 9e7a019..f6f0d2a 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceConditionsDataDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceConditionsDataDto.java @@ -29,7 +29,7 @@ public class PublicServiceConditionsDataDto { @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class Data { @JsonProperty("서비스ID") - private long serviceId; + private String serviceId; @JsonProperty("JA0101") private String targetGenderMale; diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDataDto.java b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDataDto.java index 25643ff..3eac2e3 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDataDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDataDto.java @@ -29,7 +29,7 @@ public class PublicServiceDataDto { @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class Data { @JsonProperty("서비스ID") - private long serviceId; + private String serviceId; @JsonProperty("서비스명") private String serviceName; diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDetailDataDto.java b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDetailDataDto.java index 742ea6c..7fb8317 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDetailDataDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/dto/PublicServiceDetailDataDto.java @@ -30,7 +30,7 @@ public class PublicServiceDetailDataDto { @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) public static class Data { @JsonProperty("서비스ID") - private long serviceId; + private String serviceId; @JsonProperty("서비스명") private String serviceName; diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/service/PublicServiceDataServiceImpl.java b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/service/PublicServiceDataServiceImpl.java index f6d8315..d10a356 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/service/PublicServiceDataServiceImpl.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/publicdata/service/PublicServiceDataServiceImpl.java @@ -36,7 +36,7 @@ public class PublicServiceDataServiceImpl implements PublicServiceDataService { private final PublicServiceDataValidate validator; private static final int DEFAULT_PAGE_SIZE = 1000; // 기본 페이지 크기를 1000으로 설정 - private final Set currentServiceIds = ConcurrentHashMap.newKeySet(); // 현재 동기화된 서비스 ID 저장 + private final Set currentServiceIds = ConcurrentHashMap.newKeySet(); // 현재 동기화된 서비스 ID 저장 /** * 공공서비스 목록 데이터 호출 (페이징 처리) @@ -145,7 +145,7 @@ public void syncPublicServiceConditionsData(PublicDataPath apiPath) { /** * 공통 데이터 동기화 메서드 - 중복 코드 제거를 위한 템플릿 메서드 */ - private long syncDataWithPaging( + private void syncDataWithPaging( PublicDataPath apiPath, BiFunction> fetcher, // 데이터 조회 함수 Function> dataExtractor, // DTO에서 데이터 추출 함수 @@ -204,7 +204,6 @@ private long syncDataWithPaging( } log.info("{} 전체 동기화 완료: 총 {}건", operationName, totalProcessed); - return totalProcessed; } /** @@ -260,12 +259,12 @@ public List upsertServiceDetailData(List entitiesToSave = new ArrayList<>(); // 서비스 ID 목록 생성 - Set serviceIds = dataList.stream() + Set serviceIds = dataList.stream() .map(PublicServiceDetailDataDto.Data::getServiceId) .collect(Collectors.toSet()); // 서비스 ID로 한 번에 조회 (N+1 문제 방지) - Map serviceMap = publicServiceRepository.findAllById(serviceIds) + Map serviceMap = publicServiceRepository.findAllById(serviceIds) .stream() .collect(Collectors.toMap(PublicService::getId, service -> service)); @@ -312,12 +311,12 @@ public List upsertSupportConditionsData(Lis List entitiesToSave = new ArrayList<>(); // 서비스 ID 목록 생성 - Set serviceIds = dataList.stream() + Set serviceIds = dataList.stream() .map(PublicServiceConditionsDataDto.Data::getServiceId) .collect(Collectors.toSet()); // 서비스 ID로 한 번에 조회 (N+1 문제 방지) - Map serviceMap = publicServiceRepository.findAllById(serviceIds) + Map serviceMap = publicServiceRepository.findAllById(serviceIds) .stream() .collect(Collectors.toMap(PublicService::getId, service -> service)); diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/controller/AnswerController.java b/src/main/java/com/hyetaekon/hyetaekon/post/controller/AnswerController.java deleted file mode 100644 index e275165..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/post/controller/AnswerController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.post.controller; - -public class AnswerController { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/controller/PostController.java b/src/main/java/com/hyetaekon/hyetaekon/post/controller/PostController.java new file mode 100644 index 0000000..bc69467 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/controller/PostController.java @@ -0,0 +1,54 @@ +package com.hyetaekon.hyetaekon.post.controller; + +import com.hyetaekon.hyetaekon.post.dto.PostDto; +import com.hyetaekon.hyetaekon.post.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + + // ✅ 게시글 생성 + @PostMapping + public ResponseEntity createPost(@RequestBody PostDto postDto) { + return ResponseEntity.ok(postService.createPost(postDto)); + } + + // ✅ 특정 게시글 조회 + @GetMapping("/{postId}") + public ResponseEntity getPost(@PathVariable Long postId) { + return ResponseEntity.ok(postService.getPostById(postId)); + } + + // ✅ 특정 카테고리의 게시글 조회 + @GetMapping("/category/{categoryId}") + public ResponseEntity> getPostsByCategory(@PathVariable Long categoryId) { + return ResponseEntity.ok(postService.getPostsByCategoryId(categoryId)); + } + + // ✅ 모든 게시글 조회 + @GetMapping + public ResponseEntity> getAllPosts() { + return ResponseEntity.ok(postService.getAllPosts()); + } + + // ✅ 게시글 수정 + @PutMapping("/{postId}") + public ResponseEntity updatePost(@PathVariable Long postId, @RequestBody PostDto postDto) { + return ResponseEntity.ok(postService.updatePost(postId, postDto)); + } + + // ✅ 게시글 삭제 (soft delete 방식 사용 가능) + @DeleteMapping("/{postId}") + public ResponseEntity deletePost(@PathVariable Long postId) { + postService.deletePost(postId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/dto/AnswerDto.java b/src/main/java/com/hyetaekon/hyetaekon/post/dto/AnswerDto.java deleted file mode 100644 index 3748fb1..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/post/dto/AnswerDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.post.dto; - -public class AnswerDto { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDto.java b/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDto.java new file mode 100644 index 0000000..34b975b --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDto.java @@ -0,0 +1,27 @@ +package com.hyetaekon.hyetaekon.post.dto; + +import lombok.Getter; +import lombok.Setter; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +public class PostDto { + private Long id; + private Long userId; + private String publicServiceId; + private String title; + private String content; + private LocalDateTime createdAt; + private LocalDateTime deletedAt; // 삭제일 추가 + private String postType; + private String serviceUrl; + private int recommendCnt; + private int viewCount; + private String urlTitle; + private String urlPath; + private String tags; + private Long categoryId; // 추가됨 + private List imageUrls; // ✅ 이미지 URL 리스트 추가 +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java b/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java index 130fd8f..f64acdd 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java +++ b/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java @@ -1,23 +1,62 @@ package com.hyetaekon.hyetaekon.post.entity; +import com.hyetaekon.hyetaekon.publicservice.entity.PublicService; import com.hyetaekon.hyetaekon.user.entity.User; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; - import java.time.LocalDateTime; +import java.util.List; @Entity @Getter @Setter +@Table(name = "post") public class Post { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long id; // 게시글 ID @ManyToOne @JoinColumn(name = "user_id") private User user; + @ManyToOne + @JoinColumn(name = "public_service_id") + private PublicService publicService; + + @Column(length = 20, nullable = false) // ✅ 제목 20자 제한 + private String title; + + @Column(length = 500, nullable = false) // ✅ 내용 500자 제한 + private String content; + + private LocalDateTime createdAt = LocalDateTime.now(); + private LocalDateTime deletedAt; + + private int recommendCnt; // 추천수 + + private int viewCount; // 조회수 + + @Column(name = "post_type") + @Enumerated(EnumType.STRING) // ✅ ENUM 타입으로 저장 (질문, 자유, 인사) + private PostType postType; + + private String serviceUrl; + + @Column(length = 12) // ✅ 관련 링크 제목 12자 제한 + private String urlTitle; + + private String urlPath; + + @Column(length = 255) + private String tags; // ✅ 태그는 최대 3개 (쉼표 구분) + + @Column(name = "category_id") + private Long categoryId; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postImages; // ✅ 게시글 이미지와 연결 } diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/entity/PostImage.java b/src/main/java/com/hyetaekon/hyetaekon/post/entity/PostImage.java new file mode 100644 index 0000000..177ac34 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/entity/PostImage.java @@ -0,0 +1,22 @@ +package com.hyetaekon.hyetaekon.post.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +public class PostImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @Column(name = "image_url", length = 255) + private String imageUrl; +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/entity/PostType.java b/src/main/java/com/hyetaekon/hyetaekon/post/entity/PostType.java new file mode 100644 index 0000000..db9cb66 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/entity/PostType.java @@ -0,0 +1,7 @@ +package com.hyetaekon.hyetaekon.post.entity; + +public enum PostType { + QUESTION, // 질문 게시판 + FREE, // 자유 게시판 + GREETING // 인사 게시판 +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/AnswerMapper.java b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/AnswerMapper.java deleted file mode 100644 index 1382ade..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/AnswerMapper.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.post.mapper; - -public interface AnswerMapper { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java new file mode 100644 index 0000000..908c493 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java @@ -0,0 +1,62 @@ +package com.hyetaekon.hyetaekon.post.mapper; + +import com.hyetaekon.hyetaekon.post.dto.PostDto; +import com.hyetaekon.hyetaekon.post.entity.Post; +import com.hyetaekon.hyetaekon.post.entity.PostImage; +import com.hyetaekon.hyetaekon.post.entity.PostType; +import com.hyetaekon.hyetaekon.publicservice.entity.PublicService; +import com.hyetaekon.hyetaekon.user.entity.User; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring") +public interface PostMapper { + + @Mapping(target = "imageUrls", expression = "java(mapPostImages(post))") + @Mapping(target = "postType", expression = "java(post.getPostType() != null ? post.getPostType().name() : null)") + PostDto toDto(Post post); + + default Post toEntity(PostDto dto) { + Post post = new Post(); + // 👇 User 객체 세팅 + User user = new User(); + user.setId(dto.getUserId()); + post.setUser(user); + + // 👇 PublicService 객체 세팅 + PublicService publicService = new PublicService(); + publicService.setId(dto.getPublicServiceId()); + post.setPublicService(publicService); + post.setTitle(dto.getTitle()); + post.setContent(dto.getContent()); + post.setPostType(dto.getPostType() != null ? PostType.valueOf(dto.getPostType()) : null); + post.setServiceUrl(dto.getServiceUrl()); + post.setUrlTitle(dto.getUrlTitle()); + post.setUrlPath(dto.getUrlPath()); + post.setTags(dto.getTags()); + post.setCategoryId(dto.getCategoryId()); + + if (dto.getImageUrls() != null) { + List images = dto.getImageUrls().stream() + .map(url -> { + PostImage img = new PostImage(); + img.setImageUrl(url); + img.setPost(post); + return img; + }).collect(Collectors.toList()); + post.setPostImages(images); + } + + return post; + } + + default List mapPostImages(Post post) { + if (post.getPostImages() == null) return null; + return post.getPostImages().stream() + .map(PostImage::getImageUrl) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/repository/PostRepository.java b/src/main/java/com/hyetaekon/hyetaekon/post/repository/PostRepository.java index 67289ba..2734c4e 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/post/repository/PostRepository.java +++ b/src/main/java/com/hyetaekon/hyetaekon/post/repository/PostRepository.java @@ -4,8 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface PostRepository extends JpaRepository { + List findByCategoryId(Long categoryId); // 추가됨 boolean existsByUser_IdAndDeletedAtIsNull(Long userId); } diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/service/AnswerService.java b/src/main/java/com/hyetaekon/hyetaekon/post/service/AnswerService.java deleted file mode 100644 index 5df41f5..0000000 --- a/src/main/java/com/hyetaekon/hyetaekon/post/service/AnswerService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.hyetaekon.hyetaekon.post.service; - -public class AnswerService { -} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/service/PostService.java b/src/main/java/com/hyetaekon/hyetaekon/post/service/PostService.java new file mode 100644 index 0000000..614f49c --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/service/PostService.java @@ -0,0 +1,81 @@ +package com.hyetaekon.hyetaekon.post.service; + +import com.hyetaekon.hyetaekon.post.dto.PostDto; +import com.hyetaekon.hyetaekon.post.entity.Post; +import com.hyetaekon.hyetaekon.post.entity.PostType; +import com.hyetaekon.hyetaekon.post.mapper.PostMapper; +import com.hyetaekon.hyetaekon.post.repository.PostRepository; +import com.hyetaekon.hyetaekon.publicservice.entity.PublicService; +import com.hyetaekon.hyetaekon.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + private final PostMapper postMapper; + + public List getAllPosts() { + return postRepository.findAll() + .stream() + .map(postMapper::toDto) + .collect(Collectors.toList()); + } + + public PostDto getPostById(Long id) { + Post post = postRepository.findById(id).orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다.")); + return postMapper.toDto(post); + } + + public List getPostsByCategoryId(Long categoryId) { // 추가됨 + return postRepository.findByCategoryId(categoryId) + .stream() + .map(postMapper::toDto) + .collect(Collectors.toList()); + } + + public PostDto createPost(PostDto postDto) { + Post post = postMapper.toEntity(postDto); + Post savedPost = postRepository.save(post); + return postMapper.toDto(savedPost); + } + + public PostDto updatePost(Long id, PostDto postDto) { + Post post = postRepository.findById(id).orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다.")); + + User user = new User(); + user.setId(postDto.getUserId()); + post.setUser(user); + + if (postDto.getPublicServiceId() != null) { + PublicService publicService = new PublicService(); + publicService.setId(postDto.getPublicServiceId()); // String 타입으로 변환됨 + post.setPublicService(publicService); + } else { + post.setPublicService(null); + } + + post.setTitle(postDto.getTitle()); + post.setContent(postDto.getContent()); + post.setPostType(PostType.valueOf(postDto.getPostType())); + post.setDeletedAt(postDto.getDeletedAt()); + post.setServiceUrl(postDto.getServiceUrl()); + post.setRecommendCnt(postDto.getRecommendCnt()); + post.setViewCount(postDto.getViewCount()); + post.setUrlTitle(postDto.getUrlTitle()); + post.setUrlPath(postDto.getUrlPath()); + post.setTags(postDto.getTags()); + post.setCategoryId(postDto.getCategoryId()); // 추가됨 + Post updatedPost = postRepository.save(post); + return postMapper.toDto(updatedPost); + } + + public void deletePost(Long id) { + postRepository.deleteById(id); + } +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/PublicServiceController.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/PublicServiceController.java index ec24017..f91e02d 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/PublicServiceController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/PublicServiceController.java @@ -56,7 +56,7 @@ public ResponseEntity> getServicesByCategory // 공공서비스 상세 조회 @GetMapping("/detail/{serviceId}") - public ResponseEntity getServiceDetail (@PathVariable("serviceId") Long serviceId) { + public ResponseEntity getServiceDetail (@PathVariable("serviceId") String serviceId) { Long userId = authenticateUser.authenticateUserId(); return ResponseEntity.ok(publicServiceHandler.getServiceDetail(serviceId, userId)); diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceDetailResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceDetailResponseDto.java index c48c6eb..09a67d3 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceDetailResponseDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceDetailResponseDto.java @@ -11,7 +11,7 @@ @Builder @ToString public class PublicServiceDetailResponseDto { - private Long publicServiceId; + private String publicServiceId; private String serviceName; private String servicePurpose; // 서비스 목적 diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceListResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceListResponseDto.java index 1a217de..fade66a 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceListResponseDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/dto/PublicServiceListResponseDto.java @@ -11,7 +11,7 @@ @Builder @ToString public class PublicServiceListResponseDto { - private Long publicServiceId; + private String publicServiceId; private String serviceName; private String summaryPurpose; diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/PublicService.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/PublicService.java index 5e8057f..6038465 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/PublicService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/PublicService.java @@ -16,7 +16,7 @@ @AllArgsConstructor public class PublicService { @Id - private Long id; + private String id; @Column(name = "service_name", nullable = false, length = 255) private String serviceName; // 서비스명 diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/ServiceCategory.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/ServiceCategory.java index 68e5dd3..e3896b6 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/ServiceCategory.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/ServiceCategory.java @@ -15,7 +15,11 @@ public enum ServiceCategory { EMPLOYMENT_STARTUP("고용·창업"), HEALTH_MEDICAL("보건·의료"), CULTURE_ENVIRONMENT("문화·환경"), - LIFE_STABILITY("생활안정"); + LIFE_STABILITY("생활안정"), + PROTECTION_CARE("보호·돌봄"), + PREGNANCY_CHILDBIRTH("임신·출산"), + OTHER("기타"); + @JsonValue private final String type; diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/PublicServiceRepository.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/PublicServiceRepository.java index 54c4701..c2128b0 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/PublicServiceRepository.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/PublicServiceRepository.java @@ -16,14 +16,14 @@ import java.util.Optional; @Repository -public interface PublicServiceRepository extends JpaRepository { +public interface PublicServiceRepository extends JpaRepository { Page findByServiceCategory(ServiceCategory category, Pageable pageable); List findTop6ByOrderByBookmarkCntDesc(); - Optional findById(long serviceId); + Optional findById(String serviceId); - int deleteByIdNotIn(List Ids); + int deleteByIdNotIn(List Ids); @Query("SELECT DISTINCT ps FROM PublicService ps " + "LEFT JOIN ps.specialGroups sg " + diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/PublicServiceHandler.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/PublicServiceHandler.java index 0a59a48..ec63664 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/PublicServiceHandler.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/PublicServiceHandler.java @@ -50,7 +50,7 @@ public Page getServicesByCategory(ServiceCategory // 서비스 상세 조회 @Transactional - public PublicServiceDetailResponseDto getServiceDetail(Long serviceId, Long userId) { + public PublicServiceDetailResponseDto getServiceDetail(String serviceId, Long userId) { PublicService service = publicServiceValidate.validateServiceById(serviceId); // 조회수 증가 diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/util/PublicServiceValidate.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/util/PublicServiceValidate.java index c7bb87f..3b92454 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/util/PublicServiceValidate.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/util/PublicServiceValidate.java @@ -17,7 +17,7 @@ public class PublicServiceValidate { public final PublicServiceRepository publicServiceRepository; public final UserRepository userRepository; - public PublicService validateServiceById(Long serviceId) { + public PublicService validateServiceById(String serviceId) { return publicServiceRepository.findById(serviceId) .orElseThrow(() -> new GlobalException(ErrorCode.SERVICE_NOT_FOUND_BY_ID)); }