diff --git a/build.gradle b/build.gradle index 3ada6a0..3c2100b 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/src/main/java/aibe/hosik/analysis/Analysis.java b/src/main/java/aibe/hosik/analysis/Analysis.java index 29361b3..03f58d8 100644 --- a/src/main/java/aibe/hosik/analysis/Analysis.java +++ b/src/main/java/aibe/hosik/analysis/Analysis.java @@ -1,6 +1,6 @@ package aibe.hosik.analysis; -import aibe.hosik.apply.Apply; +import aibe.hosik.apply.entity.Apply; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/aibe/hosik/apply/ApplyController.java b/src/main/java/aibe/hosik/apply/controller/ApplyController.java similarity index 81% rename from src/main/java/aibe/hosik/apply/ApplyController.java rename to src/main/java/aibe/hosik/apply/controller/ApplyController.java index d54c411..f96b223 100644 --- a/src/main/java/aibe/hosik/apply/ApplyController.java +++ b/src/main/java/aibe/hosik/apply/controller/ApplyController.java @@ -1,5 +1,6 @@ -package aibe.hosik.apply; +package aibe.hosik.apply.controller; +import aibe.hosik.apply.service.ApplyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/aibe/hosik/apply/Apply.java b/src/main/java/aibe/hosik/apply/entity/Apply.java similarity index 95% rename from src/main/java/aibe/hosik/apply/Apply.java rename to src/main/java/aibe/hosik/apply/entity/Apply.java index df6f9c9..4ffc882 100644 --- a/src/main/java/aibe/hosik/apply/Apply.java +++ b/src/main/java/aibe/hosik/apply/entity/Apply.java @@ -1,4 +1,4 @@ -package aibe.hosik.apply; +package aibe.hosik.apply.entity; import aibe.hosik.common.TimeEntity; import aibe.hosik.post.entity.Post; diff --git a/src/main/java/aibe/hosik/apply/ApplyRepository.java b/src/main/java/aibe/hosik/apply/repository/ApplyRepository.java similarity index 55% rename from src/main/java/aibe/hosik/apply/ApplyRepository.java rename to src/main/java/aibe/hosik/apply/repository/ApplyRepository.java index 78a9932..ee8552c 100644 --- a/src/main/java/aibe/hosik/apply/ApplyRepository.java +++ b/src/main/java/aibe/hosik/apply/repository/ApplyRepository.java @@ -1,6 +1,9 @@ -package aibe.hosik.apply; +package aibe.hosik.apply.repository; +import aibe.hosik.apply.entity.Apply; import org.springframework.data.jpa.repository.JpaRepository; public interface ApplyRepository extends JpaRepository { + // post id로 지원서 조회 + } diff --git a/src/main/java/aibe/hosik/apply/ApplyService.java b/src/main/java/aibe/hosik/apply/service/ApplyService.java similarity index 73% rename from src/main/java/aibe/hosik/apply/ApplyService.java rename to src/main/java/aibe/hosik/apply/service/ApplyService.java index c7ccb5a..9dbdf32 100644 --- a/src/main/java/aibe/hosik/apply/ApplyService.java +++ b/src/main/java/aibe/hosik/apply/service/ApplyService.java @@ -1,5 +1,6 @@ -package aibe.hosik.apply; +package aibe.hosik.apply.service; +import aibe.hosik.apply.repository.ApplyRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/aibe/hosik/post/PostController.java b/src/main/java/aibe/hosik/post/PostController.java deleted file mode 100644 index 18e9fa9..0000000 --- a/src/main/java/aibe/hosik/post/PostController.java +++ /dev/null @@ -1,14 +0,0 @@ -package aibe.hosik.post; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequestMapping("/api/posts") -@RequiredArgsConstructor -public class PostController { - private final PostService postService; -} diff --git a/src/main/java/aibe/hosik/post/PostRepository.java b/src/main/java/aibe/hosik/post/PostRepository.java deleted file mode 100644 index b69a679..0000000 --- a/src/main/java/aibe/hosik/post/PostRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package aibe.hosik.post; - -import aibe.hosik.post.entity.Post; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PostRepository extends JpaRepository { -} diff --git a/src/main/java/aibe/hosik/post/PostService.java b/src/main/java/aibe/hosik/post/PostService.java deleted file mode 100644 index bef8fae..0000000 --- a/src/main/java/aibe/hosik/post/PostService.java +++ /dev/null @@ -1,12 +0,0 @@ -package aibe.hosik.post; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class PostService { - private final PostRepository postRepository; -} diff --git a/src/main/java/aibe/hosik/post/controller/PostController.java b/src/main/java/aibe/hosik/post/controller/PostController.java new file mode 100644 index 0000000..00898b4 --- /dev/null +++ b/src/main/java/aibe/hosik/post/controller/PostController.java @@ -0,0 +1,68 @@ +package aibe.hosik.post.controller; + +import aibe.hosik.post.dto.PostDetailDTO; +import aibe.hosik.post.dto.PostRequestDTO; +import aibe.hosik.post.dto.PostResponseDTO; +import aibe.hosik.post.entity.Post; +import aibe.hosik.post.service.PostService; +import aibe.hosik.skill.repository.PostSkillRepository; +import aibe.hosik.user.User; +import aibe.hosik.user.UserRepository; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.Response; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +// 테스트 +@Slf4j +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +@Tag(name = "Post", description = "모집글 API") // Swagger Tag +public class PostController { + private final PostService postService; + private final UserRepository userRepository; + private final PostSkillRepository postSkillRepository; + + @Operation(summary="모집글 등록", description="모집글을 등록합니다.") + @PostMapping + public ResponseEntity createPost(@RequestBody PostRequestDTO dto, @AuthenticationPrincipal User user){ + Post createPost = postService.createPost(dto, user); + // 스킬 조회 + List skills = postSkillRepository.findSkillByPostId(createPost.getId()); + // dto 반환 + // TODO : currentCount 로직 구현 후 변환 + PostResponseDTO responseDTO = PostResponseDTO.from(createPost, skills, 0); + return ResponseEntity.ok(responseDTO); + } + // 테스트용 + @Operation(summary="모집글 등록 테스트", description="[TEST] 모집글을 등록합니다.") + @PostMapping("/mock") + public ResponseEntity createPostForSwagger(@RequestBody PostRequestDTO dto){ + + // 테스트용 userId + User mockUser = userRepository.findById(1L).orElseThrow(); + Post createPost = postService.createPost(dto, mockUser); + List skills = postSkillRepository.findSkillByPostId(createPost.getId()); + PostResponseDTO responseDTO = PostResponseDTO.from(createPost, skills, 0); + return ResponseEntity.ok(responseDTO); + } + + @Operation(summary="모집글 조회", description = "모집글 목록을 조회합니다.") + @GetMapping + public ResponseEntity> getAllPosts(){ + return ResponseEntity.ok(postService.getAllPosts()); + } + + @Operation(summary="모집글 상세 조회", description="모집글 게시글을 상세 조회합니다") + @GetMapping("/{postId}") + public ResponseEntity getPostDetail(@PathVariable Long postId){ + return ResponseEntity.ok(postService.getPostDetail(postId)); + } +} diff --git a/src/main/java/aibe/hosik/post/dto/MatchedUserDTO.java b/src/main/java/aibe/hosik/post/dto/MatchedUserDTO.java new file mode 100644 index 0000000..22582b2 --- /dev/null +++ b/src/main/java/aibe/hosik/post/dto/MatchedUserDTO.java @@ -0,0 +1,15 @@ +package aibe.hosik.post.dto; + +import aibe.hosik.post.entity.Post; + +import java.util.List; + +public record MatchedUserDTO( + Long userId, + String username, + String nickname, + String image, + String introduction +) { + +} diff --git a/src/main/java/aibe/hosik/post/dto/PostDetailDTO.java b/src/main/java/aibe/hosik/post/dto/PostDetailDTO.java new file mode 100644 index 0000000..510b4c1 --- /dev/null +++ b/src/main/java/aibe/hosik/post/dto/PostDetailDTO.java @@ -0,0 +1,41 @@ +package aibe.hosik.post.dto; + +import aibe.hosik.post.entity.Post; +import aibe.hosik.post.entity.PostCategory; +import aibe.hosik.post.entity.PostType; + +import java.time.LocalDate; +import java.util.List; + +public record PostDetailDTO( + Long id, + String title, + String content, + Integer headCount, + String image, + String requirementPersonality, + LocalDate endedAt, + + String category, + String type, + + List skills, + + // 현재 선택된 목록 보여주기 + List matchedUsers +) { + public static PostDetailDTO from(Post post, List skills, List matchedUsers) { + return new PostDetailDTO( + post.getId(), + post.getTitle(), + post.getContent(), + post.getHeadCount(), + post.getImage(), + post.getRequirementPersonality(), + post.getEndedAt(), + post.getCategory().toString(), + post.getType().toString(), + skills, + matchedUsers + ); +}} diff --git a/src/main/java/aibe/hosik/post/dto/PostRequestDTO.java b/src/main/java/aibe/hosik/post/dto/PostRequestDTO.java new file mode 100644 index 0000000..5115925 --- /dev/null +++ b/src/main/java/aibe/hosik/post/dto/PostRequestDTO.java @@ -0,0 +1,37 @@ +package aibe.hosik.post.dto; + +import aibe.hosik.post.entity.Post; +import aibe.hosik.post.entity.PostCategory; +import aibe.hosik.post.entity.PostType; +import aibe.hosik.user.User; + +import java.time.LocalDate; +import java.util.List; + +public record PostRequestDTO( + String title, + String content, + Integer headCount, + String image, + String requirementPersonality, + LocalDate endedAt, + + PostCategory category, + PostType type, + + List skills +) { + public Post toEntity(User user) { + return Post.builder() + .title(title()) + .content(content()) + .headCount(headCount()) + .image(image()) + .requirementPersonality(requirementPersonality()) + .endedAt(endedAt()) + .category(category()) + .type(type()) + .user(user) + .build(); + } +} diff --git a/src/main/java/aibe/hosik/post/dto/PostResponseDTO.java b/src/main/java/aibe/hosik/post/dto/PostResponseDTO.java new file mode 100644 index 0000000..df30312 --- /dev/null +++ b/src/main/java/aibe/hosik/post/dto/PostResponseDTO.java @@ -0,0 +1,29 @@ +package aibe.hosik.post.dto; + +import aibe.hosik.post.entity.Post; + +import java.util.List; + +public record PostResponseDTO( + Long id, + String image, + String title, + String content, + String category, + List skills, + Integer headCount, + Integer currentCount +) { + + public static PostResponseDTO from(Post post,List skills, Integer currentCount) { + return new PostResponseDTO(post.getId(), + post.getImage(), + post.getTitle(), + post.getContent(), + post.getCategory().toString(), + skills, + post.getHeadCount(), + currentCount + ); + } +} diff --git a/src/main/java/aibe/hosik/post/entity/Post.java b/src/main/java/aibe/hosik/post/entity/Post.java index 75fac1c..cfbc4ea 100644 --- a/src/main/java/aibe/hosik/post/entity/Post.java +++ b/src/main/java/aibe/hosik/post/entity/Post.java @@ -1,11 +1,14 @@ package aibe.hosik.post.entity; import aibe.hosik.common.TimeEntity; +import aibe.hosik.skill.entity.PostSkill; import aibe.hosik.user.User; import jakarta.persistence.*; import lombok.*; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; @Entity @NoArgsConstructor @@ -34,6 +37,9 @@ public class Post extends TimeEntity { @Column private String image; + @Column + private String requirementPersonality; + @Column(nullable = false) private LocalDate endedAt; @@ -47,4 +53,9 @@ public class Post extends TimeEntity { @ManyToOne(fetch = FetchType.LAZY) private User user; + + // 양방향 매핑 + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List postSkills = new ArrayList<>(); } diff --git a/src/main/java/aibe/hosik/post/repository/PostRepository.java b/src/main/java/aibe/hosik/post/repository/PostRepository.java new file mode 100644 index 0000000..a77f9a6 --- /dev/null +++ b/src/main/java/aibe/hosik/post/repository/PostRepository.java @@ -0,0 +1,23 @@ +package aibe.hosik.post.repository; + +import aibe.hosik.post.entity.Post; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +public interface PostRepository extends JpaRepository { + // Post 조회 시 postSkills, skill 엔티티 즉시 로딩 지정 + //@EntityGraph(attributePaths = {"postSkills", "postSkills.skill"}) + @Query("SELECT DISTINCT p FROM Post p LEFT JOIN FETCH p.postSkills ps LEFT JOIN FETCH ps.skill") + List findAllWithSkills(); + + // PostDetail 조회 시 즉시 로딩 지정 + //@EntityGraph(attributePaths = {"postSkills", "postSkills.skill"}) + @Query("SELECT DISTINCT p FROM Post p LEFT JOIN FETCH p.postSkills ps LEFT JOIN FETCH ps.skill WHERE p.id = :id") + Optional findByIdWithSkills(@Param("id") Long id); +} diff --git a/src/main/java/aibe/hosik/post/service/PostService.java b/src/main/java/aibe/hosik/post/service/PostService.java new file mode 100644 index 0000000..a693e60 --- /dev/null +++ b/src/main/java/aibe/hosik/post/service/PostService.java @@ -0,0 +1,15 @@ +package aibe.hosik.post.service; + +import aibe.hosik.post.dto.PostDetailDTO; +import aibe.hosik.post.dto.PostRequestDTO; +import aibe.hosik.post.dto.PostResponseDTO; +import aibe.hosik.post.entity.Post; +import aibe.hosik.user.User; + +import java.util.List; + +public interface PostService { + List getAllPosts(); + Post createPost(PostRequestDTO dto, User user); + PostDetailDTO getPostDetail(Long postId); +} diff --git a/src/main/java/aibe/hosik/post/service/PostServiceImpl.java b/src/main/java/aibe/hosik/post/service/PostServiceImpl.java new file mode 100644 index 0000000..793164e --- /dev/null +++ b/src/main/java/aibe/hosik/post/service/PostServiceImpl.java @@ -0,0 +1,92 @@ +package aibe.hosik.post.service; + +import aibe.hosik.post.dto.MatchedUserDTO; +import aibe.hosik.post.dto.PostDetailDTO; +import aibe.hosik.post.dto.PostRequestDTO; +import aibe.hosik.post.dto.PostResponseDTO; +import aibe.hosik.post.entity.Post; +import aibe.hosik.post.repository.PostRepository; +import aibe.hosik.skill.repository.PostSkillRepository; +import aibe.hosik.skill.repository.SkillRepository; +import aibe.hosik.skill.entity.PostSkill; +import aibe.hosik.skill.entity.Skill; +import aibe.hosik.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PostServiceImpl implements PostService { + private final PostRepository postRepository; + private final SkillRepository skillRepository; + private final PostSkillRepository postSkillRepository; + + // PostResponseDTO를 통해 전체 게시글(Post)를 조회합니다 + @Override + public List getAllPosts() { + // findAllWithSkills로 한 번에 fetch(post, postSkills, skill) + List posts = postRepository.findAllWithSkills(); + + return posts.stream() + .map(post -> { + // fetch된 postSkills에서 skill 추출 + List skills = post.getPostSkills().stream() + .map(s -> s.getSkill().getName()) + .collect(Collectors.toList()); + + // TODO : 현재 참여자 수 계산 + Integer currentCount = 0; + + // DTO 정적 팩토리 메서드 활용 + return PostResponseDTO.from(post, skills, currentCount); + }).collect(Collectors.toList()); + } + + // 주어진 PostRequestDTO와 User를 기반으로 새로운 게시글(Post)을 생성하고 저장합니다. + // 요청된 스킬 리스트를 기준으로 스킬을 찾거나 새로 생성하여 Post와 연결합니다. + @Override + public Post createPost(PostRequestDTO dto, User user) { + // toEntity 사용해서 Post 객체 생성 + Post post = dto.toEntity(user); + // 생성한 객체 Post 저장 + Post savePost = postRepository.save(post); + + // Skill 찾거나 생성 + // 추후 stream으로 전환 + for(String skillName : dto.skills()) { + Skill skill = skillRepository.findByName(skillName) + .orElseGet(() -> skillRepository.save(Skill.builder().name(skillName).build())); + + PostSkill postSkill = PostSkill.builder() + .post(savePost) + .skill(skill) + .build(); + + // post-skill 연관관계 추가 + postSkillRepository.save(postSkill); + } + return savePost; + } + + @Override + public PostDetailDTO getPostDetail(Long postId) { + // 게시글 정보 조회 + Post post = postRepository.findByIdWithSkills(postId) + .orElseThrow(); + + // 스킬 이름 조회 + List skills = post.getPostSkills().stream() + .map(s -> s.getSkill().getName()) + .collect(Collectors.toList()); + + // 매칭 사용자 정보 조회 + // TODO : 실제 매칭된 사용자 조회 + List matchedUsers = new ArrayList<>(); + + return PostDetailDTO.from(post,skills, matchedUsers); + } +} diff --git a/src/main/java/aibe/hosik/skill/SkillController.java b/src/main/java/aibe/hosik/skill/cotnroller/SkillController.java similarity index 81% rename from src/main/java/aibe/hosik/skill/SkillController.java rename to src/main/java/aibe/hosik/skill/cotnroller/SkillController.java index 41d08f9..1dbdf3a 100644 --- a/src/main/java/aibe/hosik/skill/SkillController.java +++ b/src/main/java/aibe/hosik/skill/cotnroller/SkillController.java @@ -1,5 +1,6 @@ -package aibe.hosik.skill; +package aibe.hosik.skill.cotnroller; +import aibe.hosik.skill.service.SkillService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/aibe/hosik/skill/repository/PostSkillRepository.java b/src/main/java/aibe/hosik/skill/repository/PostSkillRepository.java new file mode 100644 index 0000000..0c41a8f --- /dev/null +++ b/src/main/java/aibe/hosik/skill/repository/PostSkillRepository.java @@ -0,0 +1,19 @@ +package aibe.hosik.skill.repository; + + +import aibe.hosik.post.entity.Post; +import aibe.hosik.skill.entity.PostSkill; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface PostSkillRepository extends JpaRepository { + // Post 엔티티로 Post 게시글과 연관된 모든 PostSkill 목록 조회 + List findByPost(Post post); + + // Post ID로 해당 글과 연관된 모든 스킬 이르 직접 조회 + @Query("SELECT s.skill.name FROM PostSkill s WHERE s.post.id = :postId") + List findSkillByPostId(@Param("postId") Long postId); +} diff --git a/src/main/java/aibe/hosik/skill/SkillRepository.java b/src/main/java/aibe/hosik/skill/repository/SkillRepository.java similarity index 60% rename from src/main/java/aibe/hosik/skill/SkillRepository.java rename to src/main/java/aibe/hosik/skill/repository/SkillRepository.java index 4565adf..2570855 100644 --- a/src/main/java/aibe/hosik/skill/SkillRepository.java +++ b/src/main/java/aibe/hosik/skill/repository/SkillRepository.java @@ -1,7 +1,10 @@ -package aibe.hosik.skill; +package aibe.hosik.skill.repository; import aibe.hosik.skill.entity.Skill; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface SkillRepository extends JpaRepository { + Optional findByName(String skillName); } diff --git a/src/main/java/aibe/hosik/skill/SkillService.java b/src/main/java/aibe/hosik/skill/service/SkillService.java similarity index 73% rename from src/main/java/aibe/hosik/skill/SkillService.java rename to src/main/java/aibe/hosik/skill/service/SkillService.java index 6f3a38d..5e681cf 100644 --- a/src/main/java/aibe/hosik/skill/SkillService.java +++ b/src/main/java/aibe/hosik/skill/service/SkillService.java @@ -1,5 +1,6 @@ -package aibe.hosik.skill; +package aibe.hosik.skill.service; +import aibe.hosik.skill.repository.SkillRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;