diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..d385fb8 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,15 @@ +# .coderabbit.yaml +language: "ko-KR" # 한국말로 설정 +early_access: false +reviews: + profile: "chill" # 리뷰 너무 빡세게는 안한다는 뜻 + request_changes_workflow: true # 코드래빗이 리뷰 끝나면 알아서 PR 승인하게끔 + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3fac5e1..9943773 100644 --- a/build.gradle +++ b/build.gradle @@ -25,14 +25,16 @@ 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-web' - compileOnly 'org.projectlombok:lombok' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/example/FixLog/config/JwtAuthenticationFilter.java b/src/main/java/com/example/FixLog/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..bfded26 --- /dev/null +++ b/src/main/java/com/example/FixLog/config/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package com.example.FixLog.config; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.util.JwtUtil; +import java.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import com.example.FixLog.exception.ErrorCode; + +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final MemberRepository memberRepository; + + public JwtAuthenticationFilter(JwtUtil jwtUtil, MemberRepository memberRepository) { + this.jwtUtil = jwtUtil; + this.memberRepository = memberRepository; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String token = resolveToken(request); + + if (token != null && jwtUtil.isTokenValid(token)) { + String email = jwtUtil.getEmailFromToken(token); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NICKNAME_NOT_FOUND)); + + Authentication auth = new UsernamePasswordAuthenticationToken(member, null, member.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/example/FixLog/config/SecurityConfig.java b/src/main/java/com/example/FixLog/config/SecurityConfig.java new file mode 100644 index 0000000..e85a32a --- /dev/null +++ b/src/main/java/com/example/FixLog/config/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.example.FixLog.config; + +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.util.JwtUtil; +import jakarta.servlet.Filter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtUtil jwtUtil; + private final MemberRepository memberRepository; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.POST, "/members/signup").permitAll() + .requestMatchers(HttpMethod.POST, "/auth/login").permitAll() + .requestMatchers(HttpMethod.GET, "/members/check-email").permitAll() + .requestMatchers(HttpMethod.GET, "/members/check-nickname").permitAll() + .requestMatchers(HttpMethod.GET, "/h2-console/**").permitAll() + .anyRequest().authenticated() + ) + .headers(headers -> headers.frameOptions(frame -> frame.disable())) // H2 콘솔용 + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public Filter jwtAuthenticationFilter() { + return new JwtAuthenticationFilter(jwtUtil, memberRepository); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + // 인증 매니저 (선택: 로그인 시 AuthenticationManager 사용 가능) + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/FixLog/controller/AuthController.java b/src/main/java/com/example/FixLog/controller/AuthController.java new file mode 100644 index 0000000..2aa535c --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/AuthController.java @@ -0,0 +1,23 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.member.LoginRequestDto; +import com.example.FixLog.dto.member.LoginResponseDto; +import com.example.FixLog.service.AuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody LoginRequestDto requestDto) { + LoginResponseDto result = authService.login(requestDto); + return ResponseEntity.ok(Response.success("로그인 성공", result)); + } +} diff --git a/src/main/java/com/example/FixLog/controller/BookmarkFolderController.java b/src/main/java/com/example/FixLog/controller/BookmarkFolderController.java new file mode 100644 index 0000000..73321d5 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/BookmarkFolderController.java @@ -0,0 +1,97 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.PageResponseDto; +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.bookmark.request.BookmarkFolderCreateRequest; +import com.example.FixLog.dto.bookmark.request.BookmarkFolderUpdateRequest; +import com.example.FixLog.dto.bookmark.request.BookmarkMoveRequest; +import com.example.FixLog.dto.bookmark.response.BookmarkFolderCreateResponse; +import com.example.FixLog.dto.bookmark.response.BookmarkFolderReadResponse; +import com.example.FixLog.dto.post.MyPostPageResponseDto; +import com.example.FixLog.service.BookmarkFolderService; +import com.example.FixLog.service.BookmarkService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/bookmark-folders") +public class BookmarkFolderController { + private final BookmarkFolderService bookmarkFolderService; + private final BookmarkService bookmarkService; + + // 북마크 폴더 생성 + @PostMapping + public ResponseEntity createFolder( + @RequestBody BookmarkFolderCreateRequest request, + @RequestParam String requesterEmail + ) { + BookmarkFolderCreateResponse response = bookmarkFolderService.createFolder(request.folderName(), requesterEmail); + + return ResponseEntity.ok(Response.success("북마크 폴더 생성 성공", response)); + } + + // 북마크 폴더 이름 수정 + @PatchMapping("/{folder_id}") + public ResponseEntity> updateFolderName( + @PathVariable Long folderId, + @RequestParam String requesterEmail, + @RequestBody BookmarkFolderUpdateRequest request + ) { + bookmarkFolderService.updateFolderName(folderId, requesterEmail, request.folderName()); + return ResponseEntity.ok(Response.success("폴더 이름 수정 완료", null)); + } + + // 북마크 폴더 이동 + @PatchMapping("/{bookmarkId}/move") + public ResponseEntity> moveBookmark( + @PathVariable Long bookmarkId, + @RequestParam String requesterEmail, + @RequestBody BookmarkMoveRequest request + ) { + bookmarkService.moveBookmarkToFolder(bookmarkId, request.folderId(), requesterEmail); + return ResponseEntity.ok(Response.success("북마크 다른 폴더로 이동 성공", null)); + } + + + // 북마크 폴더 삭제 + @DeleteMapping("/{folderId}") + public ResponseEntity> deleteFolder( + @PathVariable Long folderId, + @RequestParam String requesterEmail + ) { + bookmarkFolderService.deleteFolder(folderId, requesterEmail); + return ResponseEntity.ok(Response.success("북마크 폴더 삭제 완료", null)); + } + + // 북마크 폴더 목록 전체 조회 - MVP + @GetMapping + public ResponseEntity>> getFolders( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size + ) { + String email = userDetails.getUsername(); + PageResponseDto response = bookmarkFolderService.getFoldersByEmail(email, page, size); + return ResponseEntity.ok(Response.success("북마크 폴더 목록 전체 조회 성공", response)); + } + + // 특정 폴더의 북마크 목록 조회 -MVP + @GetMapping("/{folderId}/bookmarks") + public ResponseEntity>> getBookmarksByFolder( + @PathVariable Long folderId, + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "6") int size, + @RequestParam(defaultValue = "0") int sort + ) { + String email = userDetails.getUsername(); + PageResponseDto data = bookmarkService.getBookmarksInFolder(email, folderId, page, sort, size); + return ResponseEntity.ok(Response.success("특정 폴더의 북마크 목록 조회 성공", data)); + } + + +} diff --git a/src/main/java/com/example/FixLog/controller/FollowController.java b/src/main/java/com/example/FixLog/controller/FollowController.java new file mode 100644 index 0000000..7f211e1 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/FollowController.java @@ -0,0 +1,65 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.follow.request.FollowRequestDto; +import com.example.FixLog.dto.follow.request.UnfollowRequestDto; +import com.example.FixLog.dto.follow.response.FollowResponseDto; +import com.example.FixLog.dto.follow.response.FollowerListResponseDto; +import com.example.FixLog.dto.follow.response.FollowingListResponseDto; +import com.example.FixLog.service.FollowService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/follow") +@RequiredArgsConstructor +public class FollowController { + private final FollowService followService; + + // 팔로우하기 + @PostMapping + public ResponseEntity> follow( + @RequestBody FollowRequestDto followRequestDto, + @AuthenticationPrincipal UserDetails userDetails // jwt 구현 전까지 임시 사용 -> 이후 AuthenticationPrincipal 사용 예정 + ){ + String requesterEmail = userDetails.getUsername(); + FollowResponseDto result = followService.follow(requesterEmail, followRequestDto.getTargetMemberId()); + return ResponseEntity.ok(Response.success("팔로우 완료", result)); + } + + // 언팔로우하기 + @PostMapping("/unfollow") + public ResponseEntity> unfollow( + @RequestBody UnfollowRequestDto requestDto, + @AuthenticationPrincipal UserDetails userDetails + ) { + String requesterEmail = userDetails.getUsername(); + followService.unfollow(requesterEmail, requestDto.getTargetMemberId()); + return ResponseEntity.ok(Response.success("언팔로우 완료", null)); + } + + // 나를 팔로우하는 목록 조회 + @GetMapping("/followers") + public ResponseEntity>> getMyFollowers( + @AuthenticationPrincipal UserDetails userDetails + ) { + String requesterEmail = userDetails.getUsername(); + List followers = followService.getMyFollowers(requesterEmail); + return ResponseEntity.ok(Response.success("나를 팔로우하는 목록 조회 성공", followers)); + } + + // 내가 팔로우하는 목록 조회 + @GetMapping("/followings") + public ResponseEntity>> getMyFollowings( + @AuthenticationPrincipal UserDetails userDetails + ) { + String requesterEmail = userDetails.getUsername(); + List followings = followService.getMyFollowings(requesterEmail); + return ResponseEntity.ok(Response.success("내가 팔로우 중인 목록 조회 성공", followings)); + } +} diff --git a/src/main/java/com/example/FixLog/controller/MainPageController.java b/src/main/java/com/example/FixLog/controller/MainPageController.java new file mode 100644 index 0000000..2551e44 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/MainPageController.java @@ -0,0 +1,31 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.main.MainPageResponseDto; +import com.example.FixLog.service.MainPageService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/main") +public class MainPageController { + private final MainPageService mainPageService; + + public MainPageController(MainPageService mainPageService){ + this.mainPageService = mainPageService; + } + + @GetMapping + public Response mainPageView(@RequestParam(value = "sort", defaultValue = "0") int sort, + @RequestParam(value = "page", defaultValue = "12") int size){ + MainPageResponseDto mainPageView = mainPageService.mainPageView(sort, size); + return Response.success("메인페이지 불러오기 성공", mainPageView); + } + + @GetMapping("/full") + public Response mainPageFullView(@RequestParam(value = "sort", defaultValue = "0") int sort, + @RequestParam(value = "page", defaultValue = "1") int page, + @RequestParam(value = "page", defaultValue = "12") int size){ + MainPageResponseDto mainPageFullView = mainPageService.mainPageFullView(sort, page, size); + return Response.success("메인페이지 전체보기 성공", mainPageFullView); + } +} diff --git a/src/main/java/com/example/FixLog/controller/MemberController.java b/src/main/java/com/example/FixLog/controller/MemberController.java new file mode 100644 index 0000000..8342511 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/MemberController.java @@ -0,0 +1,58 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.member.MemberInfoResponseDto; +import com.example.FixLog.dto.member.SignupRequestDto; +import com.example.FixLog.dto.member.DuplicateCheckResponseDto; +import com.example.FixLog.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/members") +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @PostMapping("/signup") + public ResponseEntity> signup(@RequestBody SignupRequestDto request) { + memberService.signup(request); + return ResponseEntity.ok(Response.success("회원가입 성공", null)); + } + + @GetMapping("/check-email") + public ResponseEntity> checkEmail(@RequestParam String email) { + boolean exists = memberService.isEmailDuplicated(email); + String msg = exists ? "이미 사용 중인 이메일입니다." : "사용 가능한 이메일입니다."; + return ResponseEntity.ok(Response.success(msg, new DuplicateCheckResponseDto(exists))); + } + + @GetMapping("/check-nickname") + public ResponseEntity> checkNickname(@RequestParam String nickname) { + boolean exists = memberService.isNicknameDuplicated(nickname); + String msg = exists ? "이미 사용 중인 닉네임입니다." : "사용 가능한 닉네임입니다."; + return ResponseEntity.ok(Response.success(msg, new DuplicateCheckResponseDto(exists))); + } + + @GetMapping("/me") + public ResponseEntity> getMyInfo(@AuthenticationPrincipal Member member) { + MemberInfoResponseDto responseDto = new MemberInfoResponseDto( + member.getEmail(), + member.getNickname(), + member.getProfileImageUrl(), + member.getBio(), + member.getSocialType() + ); + return ResponseEntity.ok(Response.success("회원 정보 조회 성공", responseDto)); + } + + @DeleteMapping("/me") + public ResponseEntity> withdraw(@AuthenticationPrincipal Member member) { + memberService.withdraw(member); + return ResponseEntity.ok(Response.success("회원 탈퇴 성공", null)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/controller/MypagePostController.java b/src/main/java/com/example/FixLog/controller/MypagePostController.java new file mode 100644 index 0000000..4635ef6 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/MypagePostController.java @@ -0,0 +1,50 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.PageResponseDto; +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.post.MyPostPageResponseDto; +import com.example.FixLog.service.MypagePostService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/mypage") +public class MypagePostController { + + private final MypagePostService mypagePostService; + + // 내가 쓴 글 보기 + @GetMapping("/posts") + public ResponseEntity>> getMyPosts( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "4") int size, + @RequestParam(defaultValue = "0") int sort + ) { + String email = userDetails.getUsername(); + PageResponseDto data = mypagePostService.getMyPosts(email, page, sort, size); + return ResponseEntity.ok(Response.success("내가 작성한 글 보기 성공", data)); + } + + // 내가 좋아요한 글 + @GetMapping("/likes") + public ResponseEntity>> getLikedPosts( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "4") int size, + @RequestParam(defaultValue = "0") int sort) { + + String email = userDetails.getUsername(); + PageResponseDto result = mypagePostService.getLikedPosts(email, page, sort, size); + return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result)); + } + + +} diff --git a/src/main/java/com/example/FixLog/controller/PostController.java b/src/main/java/com/example/FixLog/controller/PostController.java new file mode 100644 index 0000000..fdd808a --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/PostController.java @@ -0,0 +1,41 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.post.PostRequestDto; +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.post.PostResponseDto; +import com.example.FixLog.service.PostService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/posts") +public class PostController { + private final PostService postService; + + public PostController(PostService postService){ + this.postService = postService; + } + + @PostMapping + public Response createPost(@RequestBody PostRequestDto postRequestDto){ + postService.createPost(postRequestDto); + return Response.success("게시글 작성 성공.", null); + } + + @GetMapping("/{postId}") + public Response viewPost(@PathVariable("postId") Long postId){ + PostResponseDto viewPost = postService.viewPost(postId); + return Response.success("게시글 조회하기 성공", viewPost); + } + + @PostMapping("/{postId}/like") + public Response togglePostLike(@PathVariable("postId") Long postId){ + String message = postService.togglePostLike(postId); + return Response.success(message, null); // 좋아요 수정하기 + } + + @PostMapping("/{postId}/bookmark") + public Response toggleBookmark(@PathVariable("postId") Long postId) { + String message = postService.toggleBookmark(postId); + return Response.success(message, null); // 북마크 수정하기 + } +} diff --git a/src/main/java/com/example/FixLog/controller/TagController.java b/src/main/java/com/example/FixLog/controller/TagController.java new file mode 100644 index 0000000..cd367e9 --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/TagController.java @@ -0,0 +1,23 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.tag.TagResponseDto; +import com.example.FixLog.service.TagService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/tags") +public class TagController { + private final TagService tagService; + + public TagController(TagService tagService){ + this.tagService = tagService; + } + + @GetMapping + public Response viewTags(@RequestParam("page") int page, + @RequestParam("size") int size){ + TagResponseDto tags = tagService.viewTags(page, size); + return Response.success("태그 모아보기 성공", tags); + } +} diff --git a/src/main/java/com/example/FixLog/domain/bookmark/Bookmark.java b/src/main/java/com/example/FixLog/domain/bookmark/Bookmark.java new file mode 100644 index 0000000..3faba00 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/bookmark/Bookmark.java @@ -0,0 +1,44 @@ +package com.example.FixLog.domain.bookmark; + +import com.example.FixLog.domain.post.Post; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Bookmark { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="bookmark_id") + private Long bookmarkId; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post postId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "folder_id") + private BookmarkFolder folderId; + + private boolean isMarked; + + public void moveToFolder(BookmarkFolder newFolder) { + this.folderId = newFolder; + } + + public Bookmark(BookmarkFolder folderId, Post postId){ + this.folderId = folderId; + this.postId = postId; + this.isMarked = true; // 객체 생성 시 true + } + + public void ToggleBookmark(boolean state){ + this.isMarked = state; + } +} diff --git a/src/main/java/com/example/FixLog/domain/bookmark/BookmarkFolder.java b/src/main/java/com/example/FixLog/domain/bookmark/BookmarkFolder.java new file mode 100644 index 0000000..ef3ff6b --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/bookmark/BookmarkFolder.java @@ -0,0 +1,46 @@ +package com.example.FixLog.domain.bookmark; + +import com.example.FixLog.domain.member.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BookmarkFolder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "folder_id") + private Long folderId; + + @Column(name = "folder_name", nullable = false) + private String folderName; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private Member userId; + + @OneToMany(mappedBy = "folderId", cascade = CascadeType.ALL, orphanRemoval = true) + private List bookmarks = new ArrayList<>(); + + // 폴더 이름 수정 메서드 + public void updateName(String newName) { + this.folderName = newName; + } + + public BookmarkFolder( Member userId, String name) { + this.userId = userId; + this.folderName = name; + } + + public BookmarkFolder(Member userId){ + this.userId = userId; + this.folderName = "default folder"; + } +} diff --git a/src/main/java/com/example/FixLog/domain/follow/Follow.java b/src/main/java/com/example/FixLog/domain/follow/Follow.java new file mode 100644 index 0000000..19a47cf --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/follow/Follow.java @@ -0,0 +1,31 @@ +package com.example.FixLog.domain.follow; + +import com.example.FixLog.domain.member.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Follow { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "follow_id") + private Long followId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "followerId", nullable = false) + private Member followerId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "following_id", nullable = false) + private Member followingId; + + public Follow(Member followerId, Member followingId) { + this.followerId = followerId; + this.followingId = followingId; + } +} diff --git a/src/main/java/com/example/FixLog/domain/fork/Fork.java b/src/main/java/com/example/FixLog/domain/fork/Fork.java new file mode 100644 index 0000000..81911ee --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/fork/Fork.java @@ -0,0 +1,31 @@ +package com.example.FixLog.domain.fork; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Fork { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "fork_id") + private Long forkId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private Member userId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post originalPostId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fork_post_id", nullable = false) + private Post forkedPostId; +} diff --git a/src/main/java/com/example/FixLog/domain/like/PostLike.java b/src/main/java/com/example/FixLog/domain/like/PostLike.java new file mode 100644 index 0000000..f4f224d --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/like/PostLike.java @@ -0,0 +1,39 @@ +package com.example.FixLog.domain.like; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "like_id", nullable = false) + private Long likeId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private Member userId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post postId; + + private boolean isLiked; + + public PostLike(Member userId, Post postId){ + this.userId = userId; + this.postId = postId; + this.isLiked = true; // 객체 생성 시 true + } + + public void ToggleLike(boolean state){ + this.isLiked = state; + } +} diff --git a/src/main/java/com/example/FixLog/domain/member/Member.java b/src/main/java/com/example/FixLog/domain/member/Member.java new file mode 100644 index 0000000..8ec21d2 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/member/Member.java @@ -0,0 +1,124 @@ +package com.example.FixLog.domain.member; + +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.post.Post; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Collection; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public class Member implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long userId; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private Boolean isDeleted = false; + + public void setIsDeleted(boolean isDeleted) { + this.isDeleted = isDeleted; + } + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SocialType socialType = SocialType.EMAIL; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column + private LocalDateTime updatedAt; + + // 프로필 사진 url, 지금은 nullable 이지만 나중에 기본값 설정 + @Column + private String profileImageUrl; + + @Column(length = 200) + private String bio; + + @OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true) + private List posts = new ArrayList<>(); + + // 북마크 폴더 + @OneToOne(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true) + private BookmarkFolder bookmarkFolderId; + // 우선은 계정 당 폴더 하나만 있는 걸로 생성 + // @OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true) + // private List bookmarkFolders = new ArrayList<>(); + + // Member 객체를 정적 팩토리 방식으로 회원가입 시에 생성하는 메서드 + // Member 객체를 정적 팩토리 방식으로 생성하는 메서드 + // Creates a Member object using a static factory method + public static Member of(String email, String password, String nickname, SocialType socialType) { + Member member = new Member(); + member.email = email; + member.password = password; + member.nickname = nickname; + member.socialType = socialType; + member.isDeleted = false; + member.profileImageUrl = "https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"; // 기본 프로필 이미지(임시) + return member; + } + + public void setProfileImageUrl(String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); // 기본 권한 + } + + @Override + public String getUsername() { + return this.email; // 로그인 시 사용할 사용자 식별자 + } + + @Override + public boolean isAccountNonExpired() { + return true; // 계정 만료 여부 (true = 사용 가능) + } + + @Override + public boolean isAccountNonLocked() { + return true; // 계정 잠금 여부 (true = 잠금 아님) + } + + @Override + public boolean isCredentialsNonExpired() { + return true; // 비밀번호 만료 여부 + } + + @Override + public boolean isEnabled() { + return !this.isDeleted; // 탈퇴 여부 기반 활성 상태 + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/domain/member/SocialType.java b/src/main/java/com/example/FixLog/domain/member/SocialType.java new file mode 100644 index 0000000..3f7c286 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/member/SocialType.java @@ -0,0 +1,6 @@ +package com.example.FixLog.domain.member; + +public enum SocialType { + EMAIL, + GITHUB +} diff --git a/src/main/java/com/example/FixLog/domain/post/Post.java b/src/main/java/com/example/FixLog/domain/post/Post.java new file mode 100644 index 0000000..bdb0ac2 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/post/Post.java @@ -0,0 +1,82 @@ +package com.example.FixLog.domain.post; + +import com.example.FixLog.domain.bookmark.Bookmark; +import com.example.FixLog.domain.like.PostLike; +import com.example.FixLog.domain.member.Member; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_id", nullable = false) + private Long postId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private Member userId; + + @Column(length = 100, nullable = false) + private String postTitle; + + @Column(columnDefinition = "TEXT") + private String coverImage; + + @Lob // 텍스트가 길어질 수 있는 필드에 사용 + @Column(nullable = false) + private String problem; + + @Lob + @Column(nullable = false) + private String errorMessage; + + @Lob + @Column(nullable = false) + private String environment; + + @Lob + @Column(nullable = false) + private String reproduceCode; + + @Lob + @Column(nullable = false) + private String solutionCode; + + @Lob + private String causeAnalysis; + + @Lob + private String referenceLink; + + @Lob + private String extraContent; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime editedAt; + + // 북마크와의 관계 + @OneToMany(mappedBy = "postId", cascade = CascadeType.ALL, orphanRemoval = true) + private List bookmarks = new ArrayList<>(); + + // 태그와의 관계 + @OneToMany(mappedBy = "postId", cascade = CascadeType.ALL, orphanRemoval = true) + private List postTags = new ArrayList<>(); + + // 좋아요와의 관계 + @OneToMany(mappedBy = "postId", cascade = CascadeType.ALL, orphanRemoval = true) + private List postLikes = new ArrayList<>(); + +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/domain/post/PostTag.java b/src/main/java/com/example/FixLog/domain/post/PostTag.java new file mode 100644 index 0000000..549fbe0 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/post/PostTag.java @@ -0,0 +1,31 @@ +package com.example.FixLog.domain.post; + +import com.example.FixLog.domain.tag.Tag; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_tag_id",nullable = false) + private Long postTagId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post postId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id", nullable = false) + private Tag tagId; + + public PostTag(Post postId, Tag tagId){ + this.postId = postId; + this.tagId = tagId; + } +} diff --git a/src/main/java/com/example/FixLog/domain/tag/Tag.java b/src/main/java/com/example/FixLog/domain/tag/Tag.java new file mode 100644 index 0000000..69fa2b6 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/tag/Tag.java @@ -0,0 +1,34 @@ +package com.example.FixLog.domain.tag; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Tag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tag_id",nullable = false) + private Long tagId; + + @Enumerated(EnumType.STRING) + private TagCategory tagCategory; + + @Column(length = 20, nullable = false) + private String tagName; + + @Column(nullable = false) + private String tagInfo; + + public static Tag of(TagCategory tagCategory, String tagName, String tagInfo) { + Tag tag = new Tag(); + tag.tagCategory = tagCategory; + tag.tagName = tagName; + tag.tagInfo = tagInfo; + return tag; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/domain/tag/TagCategory.java b/src/main/java/com/example/FixLog/domain/tag/TagCategory.java new file mode 100644 index 0000000..19458c2 --- /dev/null +++ b/src/main/java/com/example/FixLog/domain/tag/TagCategory.java @@ -0,0 +1,21 @@ +package com.example.FixLog.domain.tag; + +import lombok.Getter; + +@Getter +public enum TagCategory { + BIG_CATEGORY("분류"), + MAJOR_CATEGORY("대분류"), + MIDDLE_CATEGORY("중분류"), + MINOR_CATEGORY("소분류"); + + private final String displayName; + + TagCategory(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/com/example/FixLog/dto/PageResponseDto.java b/src/main/java/com/example/FixLog/dto/PageResponseDto.java new file mode 100644 index 0000000..8cfc04e --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/PageResponseDto.java @@ -0,0 +1,35 @@ +package com.example.FixLog.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.function.Function; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PageResponseDto { + + private final List content; + private final int page; + private final int size; + private final long totalElements; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final Integer totalPages; + + private PageResponseDto(Page page) { + this.content = page.getContent(); + this.page = page.getNumber() + 1; // 1부터 시작 + this.size = page.getSize(); + this.totalElements = page.getTotalElements(); + this.totalPages = page.getTotalPages() == 0 ? null : page.getTotalPages(); + } + + public static PageResponseDto from(Page page, Function mapper) { + Page mapped = page.map(mapper); + return new PageResponseDto<>(mapped); + } +} + diff --git a/src/main/java/com/example/FixLog/dto/Response.java b/src/main/java/com/example/FixLog/dto/Response.java new file mode 100644 index 0000000..6c93365 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/Response.java @@ -0,0 +1,26 @@ +package com.example.FixLog.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; + +@JsonInclude(JsonInclude.Include.NON_NULL) // null 값을 가지는 필드는 Json 응답에 미포함 +@Getter +public class Response { + private boolean success; + private String message; + private T data; + + private Response(boolean success, String message, T data) { + this.success = success; + this.message = message; + this.data = data; + } + + public static Response success(String message, T data) { + return new Response<>(true, message, data); + } + + public static Response fail(String message) { + return new Response<>(false, message, null); + } +} diff --git a/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkFolderCreateRequest.java b/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkFolderCreateRequest.java new file mode 100644 index 0000000..f9908d9 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkFolderCreateRequest.java @@ -0,0 +1,8 @@ +package com.example.FixLog.dto.bookmark.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record BookmarkFolderCreateRequest( + @JsonProperty("name") String folderName +) {} + diff --git a/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkFolderUpdateRequest.java b/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkFolderUpdateRequest.java new file mode 100644 index 0000000..8951508 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkFolderUpdateRequest.java @@ -0,0 +1,8 @@ +package com.example.FixLog.dto.bookmark.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record BookmarkFolderUpdateRequest( + @JsonProperty("name") String folderName +) {} + diff --git a/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkMoveRequest.java b/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkMoveRequest.java new file mode 100644 index 0000000..22876dd --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/bookmark/request/BookmarkMoveRequest.java @@ -0,0 +1,7 @@ +package com.example.FixLog.dto.bookmark.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record BookmarkMoveRequest( + @JsonProperty("folder_id") Long folderId +) {} diff --git a/src/main/java/com/example/FixLog/dto/bookmark/response/BookmarkFolderCreateResponse.java b/src/main/java/com/example/FixLog/dto/bookmark/response/BookmarkFolderCreateResponse.java new file mode 100644 index 0000000..b867121 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/bookmark/response/BookmarkFolderCreateResponse.java @@ -0,0 +1,3 @@ +package com.example.FixLog.dto.bookmark.response; + +public record BookmarkFolderCreateResponse(Long folder_id, String name) {} diff --git a/src/main/java/com/example/FixLog/dto/bookmark/response/BookmarkFolderReadResponse.java b/src/main/java/com/example/FixLog/dto/bookmark/response/BookmarkFolderReadResponse.java new file mode 100644 index 0000000..0c5a698 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/bookmark/response/BookmarkFolderReadResponse.java @@ -0,0 +1,9 @@ +package com.example.FixLog.dto.bookmark.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record BookmarkFolderReadResponse( + @JsonProperty("folder_id") Long folderId, + @JsonProperty("name") String name +) {} + diff --git a/src/main/java/com/example/FixLog/dto/follow/request/FollowRequestDto.java b/src/main/java/com/example/FixLog/dto/follow/request/FollowRequestDto.java new file mode 100644 index 0000000..43b9561 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/follow/request/FollowRequestDto.java @@ -0,0 +1,12 @@ +package com.example.FixLog.dto.follow.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class FollowRequestDto { + @JsonProperty("target_member_id") + private Long targetMemberId; // 팔로우 대상 ID +} diff --git a/src/main/java/com/example/FixLog/dto/follow/request/UnfollowRequestDto.java b/src/main/java/com/example/FixLog/dto/follow/request/UnfollowRequestDto.java new file mode 100644 index 0000000..a321ee1 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/follow/request/UnfollowRequestDto.java @@ -0,0 +1,12 @@ +package com.example.FixLog.dto.follow.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UnfollowRequestDto { + @JsonProperty("target_member_id") + private Long targetMemberId; +} diff --git a/src/main/java/com/example/FixLog/dto/follow/response/FollowResponseDto.java b/src/main/java/com/example/FixLog/dto/follow/response/FollowResponseDto.java new file mode 100644 index 0000000..8eb8513 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/follow/response/FollowResponseDto.java @@ -0,0 +1,17 @@ +package com.example.FixLog.dto.follow.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FollowResponseDto { + @JsonProperty("follow_id") + private Long followId; + + @JsonProperty("following_id") + private Long followingId; + + private String nickname; +} diff --git a/src/main/java/com/example/FixLog/dto/follow/response/FollowerListResponseDto.java b/src/main/java/com/example/FixLog/dto/follow/response/FollowerListResponseDto.java new file mode 100644 index 0000000..2c01cb7 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/follow/response/FollowerListResponseDto.java @@ -0,0 +1,17 @@ +package com.example.FixLog.dto.follow.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FollowerListResponseDto { + @JsonProperty("follow_id") + private Long followId; + + @JsonProperty("follower_id") + private Long followerId; + + private String nickname; +} diff --git a/src/main/java/com/example/FixLog/dto/follow/response/FollowingListResponseDto.java b/src/main/java/com/example/FixLog/dto/follow/response/FollowingListResponseDto.java new file mode 100644 index 0000000..02b4a6c --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/follow/response/FollowingListResponseDto.java @@ -0,0 +1,17 @@ +package com.example.FixLog.dto.follow.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FollowingListResponseDto { + @JsonProperty("follow_id") + private Long followId; + + @JsonProperty("following_id") + private Long followingId; + + private String nickname; +} diff --git a/src/main/java/com/example/FixLog/dto/main/MainPagePostResponseDto.java b/src/main/java/com/example/FixLog/dto/main/MainPagePostResponseDto.java new file mode 100644 index 0000000..41755cd --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/main/MainPagePostResponseDto.java @@ -0,0 +1,19 @@ +package com.example.FixLog.dto.main; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@AllArgsConstructor +public class MainPagePostResponseDto { + private String postTitle; + private String coverImage; + private List tags; + private String writerProfileImageUrl; + private String nickname; + private LocalDate createdAt; // 여기서는 LocalDateTime 까진 필요 없으니까 + private int likeCount; +} diff --git a/src/main/java/com/example/FixLog/dto/main/MainPageResponseDto.java b/src/main/java/com/example/FixLog/dto/main/MainPageResponseDto.java new file mode 100644 index 0000000..9532959 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/main/MainPageResponseDto.java @@ -0,0 +1,25 @@ +package com.example.FixLog.dto.main; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class MainPageResponseDto { + private String userProfileImageUrl; + + private List posts; + + @JsonInclude(JsonInclude.Include.NON_DEFAULT) // totalPage = 0 인 경우에는 출력되지 않도록 + private int totalPages; + + public MainPageResponseDto(String userProfileImageUrl, List posts){ + this.userProfileImageUrl = userProfileImageUrl; + this.posts = posts; + } +} diff --git a/src/main/java/com/example/FixLog/dto/member/DuplicateCheckResponseDto.java b/src/main/java/com/example/FixLog/dto/member/DuplicateCheckResponseDto.java new file mode 100644 index 0000000..d896d70 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/DuplicateCheckResponseDto.java @@ -0,0 +1,10 @@ +package com.example.FixLog.dto.member; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class DuplicateCheckResponseDto { + private boolean duplicated; +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/dto/member/LoginRequestDto.java b/src/main/java/com/example/FixLog/dto/member/LoginRequestDto.java new file mode 100644 index 0000000..f1fd07f --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/LoginRequestDto.java @@ -0,0 +1,9 @@ +package com.example.FixLog.dto.member; + +import lombok.Getter; + +@Getter +public class LoginRequestDto { + private String email; + private String password; +} diff --git a/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java b/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java new file mode 100644 index 0000000..fb7f80c --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/LoginResponseDto.java @@ -0,0 +1,15 @@ +package com.example.FixLog.dto.member; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@AllArgsConstructor +public class LoginResponseDto { + private Long userId; + private String accessToken; + private String nickname; + private String profileImageUrl; +} diff --git a/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java b/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java new file mode 100644 index 0000000..45ced06 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/MemberInfoResponseDto.java @@ -0,0 +1,15 @@ +package com.example.FixLog.dto.member; + +import com.example.FixLog.domain.member.SocialType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MemberInfoResponseDto { + private String email; + private String nickname; + private String profileImageUrl; + private String bio; + private SocialType socialType; +} diff --git a/src/main/java/com/example/FixLog/dto/member/SignupRequestDto.java b/src/main/java/com/example/FixLog/dto/member/SignupRequestDto.java new file mode 100644 index 0000000..60fac74 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/member/SignupRequestDto.java @@ -0,0 +1,13 @@ +package com.example.FixLog.dto.member; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SignupRequestDto { + private String email; + //비밀번호 재확인은 프론트단에서 확인 + private String password; + private String nickname; +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/dto/post/MyPostPageResponseDto.java b/src/main/java/com/example/FixLog/dto/post/MyPostPageResponseDto.java new file mode 100644 index 0000000..6089085 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/post/MyPostPageResponseDto.java @@ -0,0 +1,43 @@ +package com.example.FixLog.dto.post; + +import com.example.FixLog.domain.post.Post; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MyPostPageResponseDto { + private Long postId; + private String postTitle; + private String postSummary; + private String imageUrl; + private List tags; + private LocalDateTime createdAt; + private int likeCount; + private int forkCount; + + public static MyPostPageResponseDto from(Post post, int forkCount) { + return MyPostPageResponseDto.builder() + .postId(post.getPostId()) + .postTitle(post.getPostTitle()) + .postSummary(generateSummary(post.getProblem())) + .imageUrl(post.getCoverImage()) + .tags(post.getPostTags().stream().map(tag -> tag.getTagId().getTagName()).toList()) + .createdAt(post.getCreatedAt()) + .likeCount(post.getPostLikes().size()) + .forkCount(forkCount) + .build(); + } + + private static String generateSummary(String content) { + if (content == null) return ""; + return content.length() > 200 ? content.substring(0, 200) + "..." : content; + } +} diff --git a/src/main/java/com/example/FixLog/dto/post/PostDto.java b/src/main/java/com/example/FixLog/dto/post/PostDto.java new file mode 100644 index 0000000..74fe3b1 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/post/PostDto.java @@ -0,0 +1,22 @@ +package com.example.FixLog.dto.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class PostDto { + private String postTitle; + private String coverImageUrl; + private String problem; + private String errorMessage; + private String environment; + private String reproduceCode; + private String solutionCode; + private String causeAnalysis; + private String referenceLink; + private String extraContent; + private List tags; +} diff --git a/src/main/java/com/example/FixLog/dto/post/PostRequestDto.java b/src/main/java/com/example/FixLog/dto/post/PostRequestDto.java new file mode 100644 index 0000000..558c662 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/post/PostRequestDto.java @@ -0,0 +1,21 @@ +package com.example.FixLog.dto.post; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class PostRequestDto { + private String postTitle; + private String coverImageUrl; + private String problem; + private String errorMessage; + private String environment; + private String reproduceCode; + private String solutionCode; + private String causeAnalysis; + private String referenceLink; + private String extraContent; + + private List tags; +} diff --git a/src/main/java/com/example/FixLog/dto/post/PostResponseDto.java b/src/main/java/com/example/FixLog/dto/post/PostResponseDto.java new file mode 100644 index 0000000..ae54324 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/post/PostResponseDto.java @@ -0,0 +1,17 @@ +package com.example.FixLog.dto.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +@AllArgsConstructor +public class PostResponseDto { + private PostDto postInfo; + + private String nickname; + private LocalDate createdAt; + private boolean isLiked; + private boolean isMarked; +} diff --git a/src/main/java/com/example/FixLog/dto/tag/TagDto.java b/src/main/java/com/example/FixLog/dto/tag/TagDto.java new file mode 100644 index 0000000..0cdc0c1 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/tag/TagDto.java @@ -0,0 +1,11 @@ +package com.example.FixLog.dto.tag; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TagDto { + private String tagName; + private String tagInfo; +} diff --git a/src/main/java/com/example/FixLog/dto/tag/TagResponseDto.java b/src/main/java/com/example/FixLog/dto/tag/TagResponseDto.java new file mode 100644 index 0000000..a91d005 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/tag/TagResponseDto.java @@ -0,0 +1,13 @@ +package com.example.FixLog.dto.tag; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class TagResponseDto { + private List tags; + private int totalPages; +} diff --git a/src/main/java/com/example/FixLog/exception/CustomException.java b/src/main/java/com/example/FixLog/exception/CustomException.java new file mode 100644 index 0000000..3c61011 --- /dev/null +++ b/src/main/java/com/example/FixLog/exception/CustomException.java @@ -0,0 +1,10 @@ +package com.example.FixLog.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class CustomException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/example/FixLog/exception/ErrorCode.java b/src/main/java/com/example/FixLog/exception/ErrorCode.java new file mode 100644 index 0000000..7f7e325 --- /dev/null +++ b/src/main/java/com/example/FixLog/exception/ErrorCode.java @@ -0,0 +1,31 @@ +package com.example.FixLog.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + USER_NICKNAME_NOT_FOUND(HttpStatus.NOT_FOUND,"존재하지 않는 사용자 아이디입니다."), + USER_EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "회원 이메일을 찾을 수 없습니다."), + EMAIL_DUPLICATED(HttpStatus.CONFLICT, "중복된 이메일입니다"), + NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "중복된 닉네임입니다"), + ALREADY_FOLLOWING(HttpStatus.CONFLICT, "이미 팔로우 중입니다"), + SELF_FOLLOW_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "자기 자신은 팔로우할 수 없습니다"), + SELF_UNFOLLOW_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "자기 자신은 언팔로우할 수 없습니다"), + FOLLOW_NOT_FOUND(HttpStatus.NOT_FOUND, "팔로우 관계가 존재하지 않습니다"), + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 게시글입니다."), + SELF_BOOKMARK_NOT_ALLOWED(HttpStatus.NOT_FOUND, "본인 글은 저장할 수 없습니다."), + BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "북마크를 찾을 수 없습니다."), + FOLDER_NOT_FOUND(HttpStatus.NOT_FOUND, "폴더를 찾을 수 없습니다."), + ACCESS_DENIED(HttpStatus.FORBIDDEN, "권한이 없습니다."), + TAG_NOT_FOUND(HttpStatus.NOT_FOUND, "없는 태그 번호입니다."), + SORT_NOT_EXIST(HttpStatus.BAD_REQUEST, "사용할 수 없는 정렬입니다."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), + REQUIRED_TAGS_MISSING(HttpStatus.BAD_REQUEST, "태그를 선택해주세요."), + REQUIRED_CONTENT_MISSING(HttpStatus.BAD_REQUEST, "필수 본문이 입력되지 않았습니다."); + + private final HttpStatus status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/exception/GlobalExceptionHandler.java b/src/main/java/com/example/FixLog/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..af2110e --- /dev/null +++ b/src/main/java/com/example/FixLog/exception/GlobalExceptionHandler.java @@ -0,0 +1,17 @@ +package com.example.FixLog.exception; + +import com.example.FixLog.dto.Response; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(CustomException.class) + protected ResponseEntity> handleDuplicateException(CustomException ex) { + ErrorCode errorCode = ex.getErrorCode(); + + ex.printStackTrace(); + return new ResponseEntity<>(Response.fail(errorCode.getMessage()), errorCode.getStatus()); + } +} diff --git a/src/main/java/com/example/FixLog/mock/BookmarkFolderTestDataInitializer.java b/src/main/java/com/example/FixLog/mock/BookmarkFolderTestDataInitializer.java new file mode 100644 index 0000000..81504ba --- /dev/null +++ b/src/main/java/com/example/FixLog/mock/BookmarkFolderTestDataInitializer.java @@ -0,0 +1,35 @@ +package com.example.FixLog.mock; + +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Order(2) +@RequiredArgsConstructor +public class BookmarkFolderTestDataInitializer implements CommandLineRunner { + + private final MemberRepository memberRepository; + private final BookmarkFolderRepository bookmarkFolderRepository; + + @Override + public void run(String... args) { + if (bookmarkFolderRepository.count() == 0) { + memberRepository.findByEmail("test1@example.com").ifPresentOrElse(member -> { + BookmarkFolder defaultFolder = new BookmarkFolder(member, "default folder"); + BookmarkFolder etcFolder = new BookmarkFolder(member, "그외 폴더"); + bookmarkFolderRepository.saveAll(List.of(defaultFolder, etcFolder)); + System.out.println("테스트용 북마크 폴더 2개 생성 완료"); + }, () -> { + System.out.println("test1@example.com 사용자가 존재하지 않아 폴더 생성 생략됨"); + }); + } + } +} + diff --git a/src/main/java/com/example/FixLog/mock/MemberTestDataInitializer.java b/src/main/java/com/example/FixLog/mock/MemberTestDataInitializer.java new file mode 100644 index 0000000..034ed92 --- /dev/null +++ b/src/main/java/com/example/FixLog/mock/MemberTestDataInitializer.java @@ -0,0 +1,40 @@ +package com.example.FixLog.mock; + +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.member.SocialType; + +import java.util.List; + +@Component +@Order(1) +@RequiredArgsConstructor +public class MemberTestDataInitializer implements CommandLineRunner { + + private final MemberRepository memberRepository; + private final BookmarkFolderRepository bookmarkFolderRepository; + // 비밀번호 암호화 인코더 + private final PasswordEncoder passwordEncoder; + + @Override + public void run(String... args) { + if (memberRepository.count() == 0) { + Member member1 = Member.of("test1@example.com", passwordEncoder.encode("1234"), "가나다", SocialType.EMAIL); + Member member2 = Member.of("test2@example.com", passwordEncoder.encode("1234"), "라마바", SocialType.EMAIL); + memberRepository.saveAll(List.of(member1, member2)); + + BookmarkFolder folder1 = new BookmarkFolder(member1); + BookmarkFolder folder2 = new BookmarkFolder(member2); + bookmarkFolderRepository.saveAll(List.of(folder1, folder2)); + + System.out.println("테스트용 멤버 2명 삽입 완료"); + } + } +} diff --git a/src/main/java/com/example/FixLog/mock/PostTestDataInitializer.java b/src/main/java/com/example/FixLog/mock/PostTestDataInitializer.java new file mode 100644 index 0000000..ccfc53f --- /dev/null +++ b/src/main/java/com/example/FixLog/mock/PostTestDataInitializer.java @@ -0,0 +1,85 @@ +package com.example.FixLog.mock; + +import com.example.FixLog.domain.bookmark.Bookmark; +import com.example.FixLog.domain.like.PostLike; +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.domain.post.PostTag; +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import com.example.FixLog.repository.bookmark.BookmarkRepository; +import com.example.FixLog.repository.like.PostLikeRepository; +import com.example.FixLog.repository.post.PostRepository; +import com.example.FixLog.repository.post.PostTagRepository; +import com.example.FixLog.repository.tag.TagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Order(4) // Member, Tag, BookmarkFolder 이후에 실행 +@RequiredArgsConstructor +public class PostTestDataInitializer implements CommandLineRunner { + + private final MemberRepository memberRepository; + private final PostRepository postRepository; + private final TagRepository tagRepository; + private final PostTagRepository postTagRepository; + private final PostLikeRepository postLikeRepository; + private final BookmarkRepository bookmarkRepository; + private final BookmarkFolderRepository bookmarkFolderRepository; + + @Override + public void run(String... args) { + if (postRepository.count() == 0) { + memberRepository.findByEmail("test1@example.com").ifPresentOrElse(member -> { + + // 1. 게시글 생성 + Post post = Post.builder() + .userId(member) + .postTitle("개발을 하다 보면 많은 에러를 만난다") + .coverImage("https://cdn.example.com/images/test1.jpg") + .problem("만나고 싶지 않다") + .errorMessage("에러메세지 ~~ ") + .environment("스프링부트") + .reproduceCode("여긴 뭘까요") + .solutionCode("해결 !!") + .causeAnalysis("이유를 모름") + .referenceLink("no_error@@.com") + .extraContent("추가 설명입니다.") + .createdAt(LocalDateTime.now()) + .editedAt(LocalDateTime.now()) + .build(); + postRepository.save(post); + + // 2. 태그 연결 + List tags = tagRepository.findAll(); + if (!tags.isEmpty()) { + List postTags = tags.subList(0, Math.min(2, tags.size())).stream() + .map(tag -> new PostTag(post, tag)) + .toList(); + postTagRepository.saveAll(postTags); + } + + // 3. 좋아요 추가 + PostLike postLike = new PostLike(member, post); + postLikeRepository.save(postLike); + + // 4. 북마크 추가 (기본 폴더 사용) + bookmarkFolderRepository.findFirstByUserId(member).ifPresent(folder -> { + Bookmark bookmark = new Bookmark(folder, post); + bookmarkRepository.save(bookmark); + }); + + System.out.println("테스트용 게시글 1개, 태그/좋아요/북마크까지 생성 완료"); + + }, () -> { + System.out.println("test1@example.com 사용자가 없어 게시글 생성 생략됨"); + }); + } + } +} diff --git a/src/main/java/com/example/FixLog/mock/TagTestDataInitializer.java b/src/main/java/com/example/FixLog/mock/TagTestDataInitializer.java new file mode 100644 index 0000000..68f1c31 --- /dev/null +++ b/src/main/java/com/example/FixLog/mock/TagTestDataInitializer.java @@ -0,0 +1,33 @@ +package com.example.FixLog.mock; + +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.repository.tag.TagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.example.FixLog.domain.tag.TagCategory.*; + +@Component +@RequiredArgsConstructor +@Order(3) +public class TagTestDataInitializer implements CommandLineRunner { + + private final TagRepository tagRepository; + + @Override + public void run(String... args) { + if (tagRepository.count() == 0) { + Tag tag1 = Tag.of(BIG_CATEGORY, "backend", "백엔드 설명"); + Tag tag2 = Tag.of(MAJOR_CATEGORY, "springboot", "스프링부트 설명"); + Tag tag3 = Tag.of(MAJOR_CATEGORY, "django", "장고 설명"); + Tag tag4 = Tag.of(MIDDLE_CATEGORY, "java", "자바 설명"); + Tag tag5 = Tag.of(MINOR_CATEGORY, "404 not found", "404 에러 설명"); + tagRepository.saveAll(List.of(tag1, tag2, tag3, tag4, tag5)); + System.out.println("임시 태그 4개 삽입 완료"); + } + } +} diff --git a/src/main/java/com/example/FixLog/repository/MemberRepository.java b/src/main/java/com/example/FixLog/repository/MemberRepository.java new file mode 100644 index 0000000..2193c39 --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/MemberRepository.java @@ -0,0 +1,11 @@ +package com.example.FixLog.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import com.example.FixLog.domain.member.Member; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); // 이메일로 회원 조회 + Optional findByNickname(String nickname); +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/repository/bookmark/BookmarkFolderRepository.java b/src/main/java/com/example/FixLog/repository/bookmark/BookmarkFolderRepository.java new file mode 100644 index 0000000..430568d --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/bookmark/BookmarkFolderRepository.java @@ -0,0 +1,17 @@ +package com.example.FixLog.repository.bookmark; + +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.member.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + + +public interface BookmarkFolderRepository extends JpaRepository { + Page findAllByUserId(Member userId, Pageable pageable); + BookmarkFolder findByUserId(Member userId); + Optional findFirstByUserId(Member userId); // 첫 번째 폴더만 가져올 때 Optional +} diff --git a/src/main/java/com/example/FixLog/repository/bookmark/BookmarkRepository.java b/src/main/java/com/example/FixLog/repository/bookmark/BookmarkRepository.java new file mode 100644 index 0000000..d751110 --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/bookmark/BookmarkRepository.java @@ -0,0 +1,20 @@ +package com.example.FixLog.repository.bookmark; + +import com.example.FixLog.domain.bookmark.Bookmark; +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.post.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 java.util.Optional; + +public interface BookmarkRepository extends JpaRepository { + Optional findByFolderIdAndPostId(BookmarkFolder folderId, Post postId); + + @EntityGraph(attributePaths = {"postId.postLikes"}) // 좋아요 수 때문 + Page findByFolderId(BookmarkFolder folder, Pageable pageable); +} diff --git a/src/main/java/com/example/FixLog/repository/follow/FollowRepository.java b/src/main/java/com/example/FixLog/repository/follow/FollowRepository.java new file mode 100644 index 0000000..bf6cf51 --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/follow/FollowRepository.java @@ -0,0 +1,15 @@ +package com.example.FixLog.repository.follow; + +import com.example.FixLog.domain.follow.Follow; +import org.springframework.data.jpa.repository.JpaRepository; +import com.example.FixLog.domain.member.Member; + +import java.util.List; +import java.util.Optional; + +public interface FollowRepository extends JpaRepository { + boolean existsByFollowerIdAndFollowingId(Member follower, Member following); // 중복 방지 - 같은 사람 다시 팔로우 + Optional findByFollowerIdAndFollowingId(Member follower, Member following); // 팔로우 관계 조회 + List findByFollowingId(Member following); // 나를 팔로우하는 사용자 조회 (팔로워들) + List findByFollowerId(Member follower); // 내가 팔로우하는 사용자 조회 (팔로잉들) +} diff --git a/src/main/java/com/example/FixLog/repository/fork/ForkRepository.java b/src/main/java/com/example/FixLog/repository/fork/ForkRepository.java new file mode 100644 index 0000000..7cb6a77 --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/fork/ForkRepository.java @@ -0,0 +1,19 @@ +package com.example.FixLog.repository.fork; + +import com.example.FixLog.domain.fork.Fork; +import com.example.FixLog.domain.post.Post; +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 ForkRepository extends JpaRepository { + @Query(""" + SELECT f.originalPostId.postId, COUNT(f) + FROM Fork f + WHERE f.originalPostId IN :posts + GROUP BY f.originalPostId.postId + """) + List countForksByOriginalPosts(@Param("posts") List posts); // 원본글 기반 포크 수 +} diff --git a/src/main/java/com/example/FixLog/repository/like/PostLikeRepository.java b/src/main/java/com/example/FixLog/repository/like/PostLikeRepository.java new file mode 100644 index 0000000..301f1ec --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/like/PostLikeRepository.java @@ -0,0 +1,15 @@ +package com.example.FixLog.repository.like; + +import com.example.FixLog.domain.like.PostLike; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PostLikeRepository extends JpaRepository { + Optional findByUserIdAndPostId(Member userId, Post postId); + Page findByUserId(Member user, Pageable pageable); +} diff --git a/src/main/java/com/example/FixLog/repository/post/PostRepository.java b/src/main/java/com/example/FixLog/repository/post/PostRepository.java new file mode 100644 index 0000000..e0dd7bc --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/post/PostRepository.java @@ -0,0 +1,20 @@ +package com.example.FixLog.repository.post; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PostRepository extends JpaRepository { + List findTop12ByOrderByCreatedAtDesc(); + List findTop12ByOrderByPostLikesDesc(); + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + Page findAllByOrderByPostLikesDesc(Pageable pageable); + + @EntityGraph(attributePaths = {"postLikes"}) + Page findByUserId(Member userId, Pageable pageable); // 좋아요수 때문 +} diff --git a/src/main/java/com/example/FixLog/repository/post/PostTagRepository.java b/src/main/java/com/example/FixLog/repository/post/PostTagRepository.java new file mode 100644 index 0000000..50c25e5 --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/post/PostTagRepository.java @@ -0,0 +1,7 @@ +package com.example.FixLog.repository.post; + +import com.example.FixLog.domain.post.PostTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostTagRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/FixLog/repository/tag/TagRepository.java b/src/main/java/com/example/FixLog/repository/tag/TagRepository.java new file mode 100644 index 0000000..29ef940 --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/tag/TagRepository.java @@ -0,0 +1,10 @@ +package com.example.FixLog.repository.tag; + +import com.example.FixLog.domain.tag.Tag; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TagRepository extends JpaRepository { + Page findAll(Pageable pageable); +} diff --git a/src/main/java/com/example/FixLog/service/AuthService.java b/src/main/java/com/example/FixLog/service/AuthService.java new file mode 100644 index 0000000..e47842d --- /dev/null +++ b/src/main/java/com/example/FixLog/service/AuthService.java @@ -0,0 +1,45 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.dto.member.LoginRequestDto; +import com.example.FixLog.dto.member.LoginResponseDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + + public LoginResponseDto login(LoginRequestDto requestDto) { + Member member = memberRepository.findByEmail(requestDto.getEmail()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NICKNAME_NOT_FOUND)); + + if (!passwordEncoder.matches(requestDto.getPassword(), member.getPassword())) { + throw new CustomException(ErrorCode.INVALID_PASSWORD); + } + + String token = jwtUtil.createToken(member.getUserId(), member.getEmail()); + + // 로그인 응답 시에도 null-safe하게 처리 + String profileUrl = member.getProfileImageUrl() != null + ? member.getProfileImageUrl() + : "https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"; + + return new LoginResponseDto( + member.getUserId(), + token, + member.getNickname(), + member.getProfileImageUrl() != null + ? member.getProfileImageUrl() + : "https://your-cdn.com/images/default-profile.png"); + } +} diff --git a/src/main/java/com/example/FixLog/service/BookmarkFolderService.java b/src/main/java/com/example/FixLog/service/BookmarkFolderService.java new file mode 100644 index 0000000..db28851 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/BookmarkFolderService.java @@ -0,0 +1,84 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.dto.PageResponseDto; +import com.example.FixLog.dto.bookmark.response.BookmarkFolderCreateResponse; +import com.example.FixLog.dto.bookmark.response.BookmarkFolderReadResponse; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +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; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BookmarkFolderService { + + private final BookmarkFolderRepository bookmarkFolderRepository; + private final MemberRepository memberRepository; + + // 북마크 폴더 생성 + public BookmarkFolderCreateResponse createFolder(String folderName, String requesterEmail) { + Member member = memberRepository.findByEmail(requesterEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + BookmarkFolder folder = new BookmarkFolder(member, folderName); + BookmarkFolder saved = bookmarkFolderRepository.save(folder); + + return new BookmarkFolderCreateResponse(saved.getFolderId(), saved.getFolderName()); // 폴더 ID, 폴더 이름 리턴 + } + + // 북마크 폴더 목록 전체 조회 + public PageResponseDto getFoldersByEmail(String email, int page, int size) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + Pageable pageable = PageRequest.of(page, size); + Page folderPage = bookmarkFolderRepository.findAllByUserId(member, pageable); + + return PageResponseDto.from(folderPage, folder -> new BookmarkFolderReadResponse(folder.getFolderId(), folder.getFolderName())); + } + + // 북마크 폴더 이름 수정 + public void updateFolderName(Long folderId, String email, String newName) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + BookmarkFolder folder = bookmarkFolderRepository.findById(folderId) + .orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND)); + + // 본인만 수정 가능 + if (!folder.getUserId().equals(member)) { + throw new CustomException(ErrorCode.ACCESS_DENIED); + } + + folder.updateName(newName); + } + + // 북마크 폴더 삭제 -> 기본 폴더는 삭제 불가인지? + public void deleteFolder(Long folderId, String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + BookmarkFolder folder = bookmarkFolderRepository.findById(folderId) + .orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND)); + + + // 본인만 삭제 가능 + if (!folder.getUserId().equals(member)) { + throw new CustomException(ErrorCode.ACCESS_DENIED); + } + + bookmarkFolderRepository.delete(folder); + } + + + + +} diff --git a/src/main/java/com/example/FixLog/service/BookmarkService.java b/src/main/java/com/example/FixLog/service/BookmarkService.java new file mode 100644 index 0000000..66a406c --- /dev/null +++ b/src/main/java/com/example/FixLog/service/BookmarkService.java @@ -0,0 +1,82 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.bookmark.Bookmark; +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.dto.PageResponseDto; +import com.example.FixLog.dto.post.MyPostPageResponseDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import com.example.FixLog.repository.bookmark.BookmarkRepository; +import com.example.FixLog.repository.fork.ForkRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BookmarkService { + + private final BookmarkFolderRepository bookmarkFolderRepository; + private final BookmarkRepository bookmarkRepository; + private final MemberRepository memberRepository; + private final ForkRepository forkRepository; + + // 북마크 폴더 이동 + public void moveBookmarkToFolder(Long bookmarkId, Long newFolderId, String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + Bookmark bookmark = bookmarkRepository.findById(bookmarkId) + .orElseThrow(() -> new CustomException(ErrorCode.BOOKMARK_NOT_FOUND)); + + // 폴더 주인만 이동 가능 + if (!bookmark.getFolderId().getUserId().equals(member)) { + throw new CustomException(ErrorCode.ACCESS_DENIED); + } + + BookmarkFolder targetFolder = bookmarkFolderRepository.findById(newFolderId) + .orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND)); + + bookmark.moveToFolder(targetFolder); + } + + // 특정 폴더의 북마크 목록 + public PageResponseDto getBookmarksInFolder(String email, Long folderId, int page, int sort, int size) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + BookmarkFolder folder = bookmarkFolderRepository.findById(folderId) + .orElseThrow(() -> new CustomException(ErrorCode.FOLDER_NOT_FOUND)); + + // 1: 오래된순, 0: 최신순 + Sort.Direction direction = (sort == 1) ? Sort.Direction.ASC : Sort.Direction.DESC; + Pageable pageable = PageRequest.of(page, size, Sort.by(direction, "postId.createdAt")); + + Page bookmarkPage = bookmarkRepository.findByFolderId(folder, pageable); + List bookmarkedPosts = bookmarkPage.map(Bookmark::getPostId).getContent(); + + // fork count 한번에 조회 + Map forkCountMap = forkRepository.countForksByOriginalPosts(bookmarkedPosts) + .stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> ((Long) row[1]).intValue() + )); + + return PageResponseDto.from(bookmarkPage.map(Bookmark::getPostId), post -> + MyPostPageResponseDto.from(post, forkCountMap.getOrDefault(post.getPostId(), 0)) + ); + } + +} diff --git a/src/main/java/com/example/FixLog/service/FollowService.java b/src/main/java/com/example/FixLog/service/FollowService.java new file mode 100644 index 0000000..c476619 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/FollowService.java @@ -0,0 +1,103 @@ +package com.example.FixLog.service; + +import com.example.FixLog.dto.follow.response.FollowResponseDto; +import com.example.FixLog.dto.follow.response.FollowerListResponseDto; +import com.example.FixLog.dto.follow.response.FollowingListResponseDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.follow.FollowRepository; +import lombok.RequiredArgsConstructor; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.follow.Follow; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FollowService { + + private final FollowRepository followRepository; + private final MemberRepository memberRepository; + + // 팔로우하기 + @Transactional + public FollowResponseDto follow(String requesterEmail, Long targetMemberId){ + Member follower = memberRepository.findByEmail(requesterEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + Member following = memberRepository.findById(targetMemberId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NICKNAME_NOT_FOUND)); + + // 자기 자신은 팔로우 불가 + if (follower.getUserId().equals(following.getUserId())) { + throw new CustomException(ErrorCode.SELF_FOLLOW_NOT_ALLOWED); + } + + // 중복 팔로우 방지 + if (followRepository.existsByFollowerIdAndFollowingId(follower, following)) { + throw new CustomException(ErrorCode.ALREADY_FOLLOWING); + } + + Follow follow = new Follow(follower, following); + Follow saved = followRepository.save(follow); + + return new FollowResponseDto(saved.getFollowId(), following.getUserId(), following.getNickname()); + } + + // 언팔로우하기 + @Transactional + public void unfollow(String requesterEmail, Long targetMemberId) { + Member follower = memberRepository.findByEmail(requesterEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + Member following = memberRepository.findById(targetMemberId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NICKNAME_NOT_FOUND)); + + // 자기 자신은 팔로우 불가 + if (follower.getUserId().equals(following.getUserId())) { + throw new CustomException(ErrorCode.SELF_FOLLOW_NOT_ALLOWED); + } + + Follow follow = followRepository.findByFollowerIdAndFollowingId(follower, following) + .orElseThrow(() -> new CustomException(ErrorCode.SELF_UNFOLLOW_NOT_ALLOWED)); + + followRepository.delete(follow); + } + + // 나를 팔로우하는 목록 조회 (팔로워들) + @Transactional(readOnly = true) + public List getMyFollowers(String requesterEmail) { + Member me = memberRepository.findByEmail(requesterEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + List follows = followRepository.findByFollowingId(me); + + return follows.stream() + .map(follow -> new FollowerListResponseDto( + follow.getFollowId(), + follow.getFollowerId().getUserId(), + follow.getFollowerId().getNickname() + )) + .toList(); + } + + // 내가 팔로우하는 목록 조회 (팔로잉들) + @Transactional(readOnly = true) + public List getMyFollowings(String requesterEmail) { + Member me = memberRepository.findByEmail(requesterEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + List follows = followRepository.findByFollowerId(me); + + return follows.stream() + .map(follow -> new FollowingListResponseDto( + follow.getFollowId(), + follow.getFollowingId().getUserId(), + follow.getFollowingId().getNickname() + )) + .toList(); + } + +} diff --git a/src/main/java/com/example/FixLog/service/MainPageService.java b/src/main/java/com/example/FixLog/service/MainPageService.java new file mode 100644 index 0000000..efcb6eb --- /dev/null +++ b/src/main/java/com/example/FixLog/service/MainPageService.java @@ -0,0 +1,111 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.dto.main.MainPagePostResponseDto; +import com.example.FixLog.dto.main.MainPageResponseDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.post.PostRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MainPageService { + private final PostRepository postRepository; + private final MemberService memberService; + + public MainPageService(PostRepository postRepository, MemberService memberService) { + this.postRepository = postRepository; + this.memberService = memberService; + } + + // 이미지 null일 때 default 사진으로 변경 (프로필 사진, + public String getDefaultImage(String image){ + String imageUrl = (image == null || image.isBlank()) + ? "https://example.com/default-cover-image.png" : image; + System.out.println(imageUrl); + return imageUrl; + } + + // 메인페이지 보기 + public MainPageResponseDto mainPageView(int sort, int size){ + // 사용자 정보 불러오기 + Member member = memberService.getCurrentMemberInfo(); + String imageUrl = member.getProfileImageUrl(); + String profileImageUrl = getDefaultImage(imageUrl); + + // 페이지 (글 12개) 불러오기 + Page posts; + Sort sortOption; + + if (sort == 0) { // 최신순 정렬 + sortOption = Sort.by(Sort.Direction.DESC, "createdAt"); + } else if (sort == 1) { // 인기순 정렬 + sortOption = Sort.by(Sort.Direction.DESC, "postLikes"); + } else + throw new CustomException(ErrorCode.SORT_NOT_EXIST); + + Pageable pageable = PageRequest.of(0, size, sortOption); + posts = postRepository.findAll(pageable); + + List postList = posts.stream() + .map(post -> new MainPagePostResponseDto( + post.getPostTitle(), + getDefaultImage(post.getCoverImage()), + post.getPostTags().stream() + .map(postTag -> postTag.getTagId().getTagName()) + .collect(Collectors.toList()), + getDefaultImage(post.getUserId().getProfileImageUrl()), + post.getUserId().getNickname(), + post.getCreatedAt().toLocalDate(), + post.getPostLikes().size() + )) + .collect(Collectors.toList()); + + return new MainPageResponseDto(profileImageUrl, postList); + } + + // 메인페이지 전체보기 + public MainPageResponseDto mainPageFullView(int sort, int page, int size){ + // 사용자 정보 불러오기 + Member member = memberService.getCurrentMemberInfo(); + String imageUrl = member.getProfileImageUrl(); + String profileImageUrl = getDefaultImage(imageUrl); + + // 페이지 설정 (한 페이지당 12개) + Pageable pageable = PageRequest.of(page - 1, size); + Page postPage; + + if (sort == 0) { // 최신순 정렬 + postPage = postRepository.findAllByOrderByCreatedAtDesc(pageable); + } else if (sort == 1) { // 인기순 정렬 + postPage = postRepository.findAllByOrderByPostLikesDesc(pageable); + } else + throw new CustomException(ErrorCode.SORT_NOT_EXIST); + + List postList = postPage.stream() + .map(post -> new MainPagePostResponseDto( + post.getPostTitle(), + getDefaultImage(post.getCoverImage()), + post.getPostTags().stream() + .map(postTag -> postTag.getTagId().getTagName()) + .collect(Collectors.toList()), + getDefaultImage(post.getUserId().getProfileImageUrl()), + post.getUserId().getNickname(), + post.getCreatedAt().toLocalDate(), + post.getPostLikes().size() + )) + .collect(Collectors.toList()); + + int totalPages = postPage.getTotalPages(); // 전체 페이지 수 출력 + + return new MainPageResponseDto(profileImageUrl, postList, totalPages); + } +} diff --git a/src/main/java/com/example/FixLog/service/MemberService.java b/src/main/java/com/example/FixLog/service/MemberService.java new file mode 100644 index 0000000..5138c30 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/MemberService.java @@ -0,0 +1,76 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.member.SocialType; +import com.example.FixLog.dto.member.SignupRequestDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final BookmarkFolderRepository bookmarkFolderRepository; + + public void signup(SignupRequestDto request) { + // 이메일 중복 검사 + if (isEmailDuplicated(request.getEmail())) { + throw new CustomException(ErrorCode.EMAIL_DUPLICATED); + } + + // 닉네임 중복 검사 + if (isNicknameDuplicated(request.getNickname())) { + throw new CustomException(ErrorCode.NICKNAME_DUPLICATED); + } + + // 문제 없으면 저장 + Member member = Member.of( + request.getEmail(), + passwordEncoder.encode(request.getPassword()), + request.getNickname(), + SocialType.EMAIL + ); + // 기본 프로필 이미지 URL 생성 + member.setProfileImageUrl("https://dummyimage.com/200x200/cccccc/ffffff&text=Profile"); + // 먼저 회원 정보 저장 + memberRepository.save(member); + + // 기본 폴더 생성 + BookmarkFolder newFolder = new BookmarkFolder(member); + bookmarkFolderRepository.save(newFolder); + + } + + public boolean isEmailDuplicated(String email) { + return memberRepository.findByEmail(email).isPresent(); + } + + public boolean isNicknameDuplicated(String nickname) { + return memberRepository.findByNickname(nickname).isPresent(); + } + + // 현재 로그인한 사용자 정보 member 객체로 반환 + public Member getCurrentMemberInfo(){ + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userEmail = authentication.getName(); + return memberRepository.findByEmail(userEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + } + + // 회원탈퇴 + public void withdraw(Member member) { + member.setIsDeleted(true); + memberRepository.save(member); + } +} + diff --git a/src/main/java/com/example/FixLog/service/MypagePostService.java b/src/main/java/com/example/FixLog/service/MypagePostService.java new file mode 100644 index 0000000..b9aa201 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/MypagePostService.java @@ -0,0 +1,91 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.like.PostLike; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.dto.PageResponseDto; +import com.example.FixLog.dto.post.MyPostPageResponseDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.fork.ForkRepository; +import com.example.FixLog.repository.like.PostLikeRepository; +import com.example.FixLog.repository.post.PostRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class MypagePostService { + + private final PostRepository postRepository; + private final MemberRepository memberRepository; + private final ForkRepository forkRepository; + private final PostLikeRepository postLikeRepository; + + public MypagePostService(PostRepository postRepository, MemberRepository memberRepository, ForkRepository forkRepository, PostLikeRepository postLikeRepository) { + this.postRepository = postRepository; + this.memberRepository = memberRepository; + this.forkRepository = forkRepository; + this.postLikeRepository = postLikeRepository; + } + + // 내가 쓴 글 보기 + public PageResponseDto getMyPosts(String email, int page, int sort, int size) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + // 1: 오래된순, 0: 최신순 + Sort.Direction direction = (sort == 1) ? Sort.Direction.ASC : Sort.Direction.DESC; + Pageable pageable = PageRequest.of(page, size, Sort.by(direction, "createdAt")); + + Page postPage = postRepository.findByUserId(member, pageable); + List posts = postPage.getContent(); + + // fork count 한번에 조회 + List forkCounts = forkRepository.countForksByOriginalPosts(posts); + Map forkCountMap = forkCounts.stream() + .collect(Collectors.toMap( + row -> (Long) row[0], // postId + row -> ((Long) row[1]).intValue() // 포크 카운트 (int) + )); + + return PageResponseDto.from(postPage, post -> + MyPostPageResponseDto.from(post, forkCountMap.getOrDefault(post.getPostId(), 0) // 없으면 0 반환 + ) + ); + } + + // 내가 좋아요한 글 보기 + public PageResponseDto getLikedPosts(String email, int page, int sort, int size) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); + + // 1: 오래된순, 0: 최신순 + Sort.Direction direction = (sort == 1) ? Sort.Direction.ASC : Sort.Direction.DESC; + Pageable pageable = PageRequest.of(page, size, Sort.by(direction, "postId.createdAt")); + + Page postLikePage = postLikeRepository.findByUserId(member, pageable); + List likedPosts = postLikePage.map(PostLike::getPostId).getContent(); + + // fork count 한번에 조회 + List forkCounts = forkRepository.countForksByOriginalPosts(likedPosts); + Map forkCountMap = forkCounts.stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> ((Long) row[1]).intValue() + )); + + return PageResponseDto.from(postLikePage.map(PostLike::getPostId), post -> + MyPostPageResponseDto.from(post, forkCountMap.getOrDefault(post.getPostId(), 0)) + ); + } + + +} diff --git a/src/main/java/com/example/FixLog/service/PostService.java b/src/main/java/com/example/FixLog/service/PostService.java new file mode 100644 index 0000000..facfa92 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/PostService.java @@ -0,0 +1,228 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.bookmark.Bookmark; +import com.example.FixLog.domain.bookmark.BookmarkFolder; +import com.example.FixLog.domain.like.PostLike; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.domain.post.PostTag; +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.domain.tag.TagCategory; +import com.example.FixLog.dto.post.PostDto; +import com.example.FixLog.dto.post.PostRequestDto; +import com.example.FixLog.dto.post.PostResponseDto; +import com.example.FixLog.exception.CustomException; +import com.example.FixLog.exception.ErrorCode; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import com.example.FixLog.repository.bookmark.BookmarkRepository; +import com.example.FixLog.repository.like.PostLikeRepository; +import com.example.FixLog.repository.post.PostRepository; +import com.example.FixLog.repository.tag.TagRepository; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +@Service +public class PostService { + private final PostRepository postRepository; + private final PostLikeRepository postLikeRepository; + private final BookmarkRepository bookmarkRepository; + private final TagRepository tagRepository; + private final BookmarkFolderRepository bookmarkFolderRepository; + private final MemberService memberService; + + public PostService(PostRepository postRepository, PostLikeRepository postLikeRepository, + BookmarkRepository bookmarkRepository, TagRepository tagRepository, + BookmarkFolderRepository bookmarkFolderRepository, MemberService memberService){ + this.postRepository = postRepository; + this.postLikeRepository = postLikeRepository; + this.bookmarkRepository = bookmarkRepository; + this.tagRepository = tagRepository; + this.bookmarkFolderRepository = bookmarkFolderRepository; + this.memberService = memberService; + } + + // 이미지 null일 때 default 사진으로 변경 (프로필 사진, + public String getDefaultImage(String image){ + String imageUrl = (image == null || image.isBlank()) + ? "https://example.com/default-cover-image.png" : image; + System.out.println(imageUrl); + return imageUrl; + } + + // 게시글 생성하기 + @Transactional + public void createPost(PostRequestDto postRequestDto){ + Member member = memberService.getCurrentMemberInfo(); + String coverImageUrl = postRequestDto.getCoverImageUrl(); + + // 북마크 카테고리별로 선택 제한 두기 + List tags = fetchAndValidateTags(postRequestDto.getTags()); + + // 게시글 발행 + validatePost(postRequestDto); // 필수 항목 다 입력되었는지 확인 + Post newPost = Post.builder() + .userId(member) + .postTitle(postRequestDto.getPostTitle()) + .coverImage(coverImageUrl) + .problem(postRequestDto.getProblem()) + .errorMessage(postRequestDto.getErrorMessage()) + .environment(postRequestDto.getEnvironment()) + .reproduceCode(postRequestDto.getReproduceCode()) + .solutionCode(postRequestDto.getSolutionCode()) + .causeAnalysis(postRequestDto.getCauseAnalysis()) + .referenceLink(postRequestDto.getReferenceLink()) + .extraContent(postRequestDto.getExtraContent()) + .createdAt(LocalDateTime.now()) + .editedAt(LocalDateTime.now()) + .postTags(new ArrayList<>()) + .build(); + + // 태그 저장 + for (Tag tag : tags) { + PostTag postTag = new PostTag(newPost, tag); + newPost.getPostTags().add(postTag); + } + postRepository.save(newPost); + } + + // 태그 다 선택 했는지 + private List fetchAndValidateTags(List tagIds){ + // 태그 ID로 Tag 엔티티 조회 + List tags = tagIds.stream() + .map(tagId -> tagRepository.findById(tagId) + .orElseThrow(() -> new CustomException(ErrorCode.TAG_NOT_FOUND))) + .toList(); + + // 태그 종류별로 그룹화 + Map> tagCategoryMap = tags.stream() + .collect(Collectors.groupingBy(Tag::getTagCategory)); + + TagCategory[] requiredTypes = TagCategory.values(); + List issues = new ArrayList<>(); + + // 하나라도 빠졌다면 예외 처리 + for (TagCategory type : requiredTypes){ + List categories = tagCategoryMap.get(type); + + if (type == TagCategory.MINOR_CATEGORY){ // 소분류는 4개까지 선택 가능 + if (categories == null) + issues.add(type.name() + "태그가 선택되지 않았습니다."); + else if (categories.size() > 4) + issues.add(type.name() + "태그는 최대 4개까지 선택 가능합니다."); + + } else { + if (categories == null) + issues.add(type.name() + "태그가 선택되지 않았습니다."); + else if (categories.size() > 1) + issues.add(type.name() + "태그는 하나만 선택해야 합니다."); + } + } + + if (!issues.isEmpty()) { + throw new CustomException(ErrorCode.REQUIRED_TAGS_MISSING); + // throw new CustomException(ErrorCode.REQUIRED_TAGS_MISSING, String.join(", ", issues)); + // throw new CustomException(ErrorCode.REQUIRED_TAGS_MISSING.withDetail(missingTypes.toString())); + } + return tags; + } + + // 게시글 필수 항목 다 작성했는지 + private void validatePost(PostRequestDto postRequestDto){ + if (postRequestDto.getPostTitle().isBlank() | postRequestDto.getProblem().isBlank() + | postRequestDto.getErrorMessage().isBlank() | postRequestDto.getEnvironment().isBlank() + | postRequestDto.getReproduceCode().isBlank() | postRequestDto.getSolutionCode().isBlank()) + throw new CustomException(ErrorCode.REQUIRED_CONTENT_MISSING); + } + + // 게시글 조회하기 + public PostResponseDto viewPost(Long postId){ + Member member = memberService.getCurrentMemberInfo(); + + Post currentPost = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + PostDto postInfo = new PostDto( + currentPost.getPostTitle(), + getDefaultImage(currentPost.getCoverImage()), + currentPost.getProblem(), + currentPost.getErrorMessage(), + currentPost.getEnvironment(), + currentPost.getReproduceCode(), + currentPost.getSolutionCode(), + currentPost.getCauseAnalysis(), + currentPost.getReferenceLink(), + currentPost.getExtraContent(), + currentPost.getPostTags().stream() + .map(postTag -> postTag.getTagId().getTagName()) + .collect(toList()) + ); + + String nickname = member.getNickname(); + LocalDate createdAt = currentPost.getCreatedAt().toLocalDate(); + boolean isLiked = currentPost.getPostLikes().stream() + .anyMatch(postLike -> postLike.getUserId().equals(member)); + boolean isMarked = currentPost.getBookmarks().stream() + .anyMatch(bookmark -> bookmark.getFolderId().getUserId().equals(member)); + + return new PostResponseDto(postInfo, nickname, createdAt, isLiked, isMarked); + } + + // 게시글 좋아요 + public String togglePostLike(Long postIdInput){ + Member member = memberService.getCurrentMemberInfo(); + + Post postId = postRepository.findById(postIdInput) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + Optional optionalLike = postLikeRepository.findByUserIdAndPostId(member, postId); + + if (optionalLike.isEmpty()){ // 객체 없는 경우 + PostLike newLike = new PostLike(member, postId); + postLikeRepository.save(newLike); + return "게시글 좋아요 성공"; + } else { // 객체 있는 경우 + PostLike postLike = optionalLike.get(); + postLike.ToggleLike(!postLike.isLiked()); + postLikeRepository.save(postLike); + if (postLike.isLiked()) + return "게시글 좋아요 성공"; + else + return "게시글 좋아요 삭제 성공"; + } + } + + // 게시글 북마크 + public String toggleBookmark(Long postIdInput){ + Member member = memberService.getCurrentMemberInfo(); + + Post postId = postRepository.findById(postIdInput) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + BookmarkFolder folderId = bookmarkFolderRepository.findByUserId(member); // 이 코드는 폴더가 하나일 때만 적용됨 + Optional optionalBookmark = bookmarkRepository.findByFolderIdAndPostId(folderId, postId); + + // 본인 글은 북마크 못하도록 + if (member == folderId.getUserId()) + throw new CustomException(ErrorCode.SELF_BOOKMARK_NOT_ALLOWED); + + // 북마크 처리 + if (optionalBookmark.isEmpty()){ // 객체 없는 경우 + Bookmark newBookmark = new Bookmark(folderId, postId); + bookmarkRepository.save(newBookmark); + return "게시글 북마크 성공"; + } else { // 객체 있는 경우 + Bookmark bookmark = optionalBookmark.get(); + bookmark.ToggleBookmark(!bookmark.isMarked()); + bookmarkRepository.save(bookmark); + System.out.println(bookmark.isMarked()); + return (bookmark.isMarked()) ? "게시글 북마크 성공" : "게시글 북마크 삭제 성공"; + } + } +} diff --git a/src/main/java/com/example/FixLog/service/TagService.java b/src/main/java/com/example/FixLog/service/TagService.java new file mode 100644 index 0000000..662a136 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/TagService.java @@ -0,0 +1,41 @@ +package com.example.FixLog.service; + +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.dto.tag.TagDto; +import com.example.FixLog.dto.tag.TagResponseDto; +import com.example.FixLog.repository.tag.TagRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class TagService { + private final TagRepository tagRepository; + + public TagService(TagRepository tagRepository){ + this.tagRepository = tagRepository; + } + + // 태그 모음 보기 + public TagResponseDto viewTags(int page, int size){ + Pageable pageable = PageRequest.of(page - 1, size); + + Page tags = tagRepository.findAll(pageable); + + List tagList = tags.stream() + .map(tag -> new TagDto( + tag.getTagName(), + tag.getTagInfo().length() > 100 + ? tag.getTagInfo().substring(0, 100) + "..." : tag.getTagInfo() + )) + .collect(Collectors.toList()); + + int totalPages = tags.getTotalPages(); + + return new TagResponseDto(tagList, totalPages); + } +} diff --git a/src/main/java/com/example/FixLog/util/JwtUtil.java b/src/main/java/com/example/FixLog/util/JwtUtil.java new file mode 100644 index 0000000..6ef7972 --- /dev/null +++ b/src/main/java/com/example/FixLog/util/JwtUtil.java @@ -0,0 +1,62 @@ +package com.example.FixLog.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secretKeyString; + + private Key secretKey; + private final long expiration = 1000 * 60 * 60 * 24; // 24시간 + + @PostConstruct + public void init() { + this.secretKey = Keys.hmacShaKeyFor(secretKeyString.getBytes()); + } + + public String createToken(Long userId, String email) { + return Jwts.builder() + .setSubject(email) + .claim("userId", userId) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public Claims getClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public boolean isTokenValid(String token) { + try { + getClaims(token); + return true; + } catch (Exception e) { + return false; + } + } + + public String getEmailFromToken(String token) { + return getClaims(token).getSubject(); + } + + public Long getUserIdFromToken(String token) { + return getClaims(token).get("userId", Long.class); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 80e40c5..e914609 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,19 @@ spring.application.name=FixLog + +# DB setting +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# DataBase Info +spring.datasource.url=jdbc:h2:tcp://localhost/~/fixlog +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect + +# JWT +jwt.secret=fixlogfixlogfixlogfixlogfixlog1234 +jwt.expiration-time=86400000 \ No newline at end of file