Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3744573
refactor: 엔티티 클래스에서 빌더패턴 대신 생성자 사용
leesewon00 Aug 8, 2024
2e34feb
feat: 연관관계 편의 메소드 작성
leesewon00 Aug 8, 2024
e211a17
feat: 커뮤니티 관련 DTO 정의
leesewon00 Aug 8, 2024
bd4f0bc
feat: 커뮤니티 레포지토리 정의
leesewon00 Aug 8, 2024
112efca
feat: 커뮤니티 서비스 로직 추가
leesewon00 Aug 8, 2024
bd7a7b9
feat: 재귀 쿼리를 위한 Transient 필드 추가 및 댓글 서비스 로직 추가
leesewon00 Aug 8, 2024
2839697
feat: 커뮤니티 컨트롤러 로직 추가
leesewon00 Aug 8, 2024
ae198ed
feat: 커뮤니티 예외 추가
leesewon00 Aug 8, 2024
213320f
feat: 게시글 조회수 동시성 처리 로직 추가
leesewon00 Aug 8, 2024
2f94da2
feat: 게시판 데이터 추가
leesewon00 Aug 8, 2024
4eee103
feat: Mockito 의존성 추가
leesewon00 Aug 8, 2024
027d0b3
test: 커뮤니티 레포지토리 테스트 추가
leesewon00 Aug 8, 2024
ebd7735
test: 커뮤니티 서비스 테스트 추가
leesewon00 Aug 8, 2024
311d870
test: 게시글 조회수 동시성 처리 테스트 추가
leesewon00 Aug 8, 2024
494f153
feat: BaseEntity 동작을 위해 @EnableScheduling 어노테이션 추가
leesewon00 Aug 8, 2024
f47170e
feat: 로컬 환경에서 도커 컨테이너 빌드 자동화하는 쉘 스크립트 작성
leesewon00 Aug 8, 2024
cc073a4
refactor: BoardCode 불일치 문제 해결
leesewon00 Aug 9, 2024
4178bcc
refactor: 함수명 컨벤션에 따라 수정
leesewon00 Aug 9, 2024
43962ac
refactor: 불필요한 Getter 삭제
leesewon00 Aug 9, 2024
0f4fcbd
feat: 스케줄링 작업 비동기로 전환
leesewon00 Aug 10, 2024
7f69f5b
feat: 조회수 갱신 작업 비동기로 전환
leesewon00 Aug 10, 2024
e295a58
feat: 조회수 조작 막는 코드 추가
leesewon00 Aug 10, 2024
495dc58
test: 조회수 조작 막는 테스트코드 추가
leesewon00 Aug 10, 2024
a3de0d8
refactor: 재사용성을 고려하여 RedisService 함수 리팩토링
leesewon00 Aug 10, 2024
fd0e2a6
refactor: 중복코드 RedisUtils 함수화
leesewon00 Aug 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ out/

### YML ###
application-secret.yml
application-prod.yml
application-prod.yml

### docker volumes ###
mysql_data_local
redis_data_local
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
testImplementation "org.mockito:mockito-core:3.3.3"

compileOnly 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -62,4 +63,4 @@ sourceSets {

compileJava {
options.annotationProcessorGeneratedSourcesDirectory = file('build/generated/sources/annotationProcessor/java/main')
}
}
14 changes: 4 additions & 10 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.8'

services:
mysql:
image: mysql:8.0
Expand All @@ -11,17 +9,13 @@ services:
MYSQL_PASSWORD: solid_connection_local_password
ports:
- "3306:3306"
# volumes:
# - mysql_data_local:/var/lib/mysql
volumes:
- ./mysql_data_local:/var/lib/mysql

redis:
image: redis:latest
container_name: solid-connection-local-redis
ports:
- "6379:6379"
# volumes:
# - redis_data_local:/data

#volumes:
# mysql_data_local:
# redis_data_local:
volumes:
- ./redis_data_local:/data
12 changes: 12 additions & 0 deletions local_compose_down.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

set -e

echo "Starting all docker containers..."
docker-compose -f docker-compose.local.yml down

echo "Pruning unused Docker images..."
docker image prune -f

echo "Containers are up and running."
docker-compose ps -a
23 changes: 23 additions & 0 deletions local_compose_up.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# 명령이 0이 아닌 종료값을 가질때 즉시 종료
set -e

if [ ! -d "mysql_data_local" ]; then
echo "mysql_data_local 디렉토리가 없습니다. 디렉토리를 생성합니다."
mkdir -p mysql_data_local
fi

if [ ! -d "redis_data_local" ]; then
echo "redis_data_local 디렉토리가 없습니다. 디렉토리를 생성합니다."
mkdir -p redis_data_local
fi

echo "Starting all docker containers..."
docker-compose -f docker-compose.local.yml up -d

echo "Pruning unused Docker images..."
docker image prune -f

echo "Containers are up and running."
docker-compose ps -a
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableJpaAuditing
@SpringBootApplication
public class SolidConnectionApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.example.solidconnection.board.controller;

import com.example.solidconnection.board.service.BoardService;
import com.example.solidconnection.post.dto.BoardFindPostResponse;
import com.example.solidconnection.type.BoardCode;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN;

@RestController
@RequiredArgsConstructor
@RequestMapping("/communities")
@SecurityRequirements
@SecurityRequirement(name = ACCESS_TOKEN)
public class BoardController {

private final BoardService boardService;

// todo: 회원별로 접근 가능한 게시판 목록 조회 기능 개발
@GetMapping()
public ResponseEntity<?> findAccessibleCodes() {
List<String> accessibleCodeList = new ArrayList<>();
for (BoardCode boardCode : BoardCode.values()) {
accessibleCodeList.add(String.valueOf(boardCode));
}
return ResponseEntity.ok().body(accessibleCodeList);
}

@GetMapping("/{code}")
public ResponseEntity<?> findPostsByCodeAndCategory(
@PathVariable(value = "code") String code,
@RequestParam(value = "category", defaultValue = "전체") String category) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 이것은 단순 궁금증 ㅎㅎ

함수 안에서 개행은 어떤 규칙으로 하시는지 궁금합니다~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별도의 규칙을 따르면서 작성하지는 않았습니다.
컨트롤러에서 받는 파라미터가 많아짐에 따라서 개행하였고,
가독성을 위해서 컨트롤러의 다른 함수들도 모두 파라미터별로 개행하였습니다.

List<BoardFindPostResponse> postsByCodeAndPostCategory = boardService
.findPostsByCodeAndPostCategory(code, category);
return ResponseEntity.ok().body(postsByCodeAndPostCategory);
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.example.solidconnection.entity;
package com.example.solidconnection.board.domain;

import com.example.solidconnection.post.domain.Post;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Board {

@Id
Expand All @@ -23,7 +19,11 @@ public class Board {
@Column(nullable = false, length = 20)
private String koreanName;

@OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> postList = new ArrayList<>();
}

public Board(String code, String koreanName) {
this.code = code;
this.koreanName = koreanName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.solidconnection.board.dto;

import com.example.solidconnection.board.domain.Board;

public record PostFindBoardResponse(
String code,
String koreanName
) {
public static PostFindBoardResponse from(Board board) {
return new PostFindBoardResponse(
board.getCode(),
board.getKoreanName()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.solidconnection.board.repository;

import com.example.solidconnection.board.domain.Board;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.custom.exception.ErrorCode;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;

import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE;

@Repository
public interface BoardRepository extends JpaRepository<Board, String> {

@EntityGraph(attributePaths = {"postList"})
Optional<Board> findBoardByCode(@Param("code") String code);

default Board getByCodeUsingEntityGraph(String code) {
return findBoardByCode(code)
.orElseThrow(() -> new CustomException(ErrorCode.INVALID_BOARD_CODE));
}

default Board getByCode(String code) {
return findById(code)
.orElseThrow(() -> new CustomException(INVALID_BOARD_CODE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.solidconnection.board.service;

import com.example.solidconnection.board.domain.Board;
import com.example.solidconnection.board.repository.BoardRepository;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.custom.exception.ErrorCode;
import com.example.solidconnection.post.domain.Post;
import com.example.solidconnection.post.dto.BoardFindPostResponse;
import com.example.solidconnection.type.BoardCode;
import com.example.solidconnection.type.PostCategory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;

private String validateCode(String code) {
try {
return String.valueOf(BoardCode.valueOf(code));
} catch (IllegalArgumentException ex) {
throw new CustomException(ErrorCode.INVALID_BOARD_CODE);
}
}

private PostCategory validatePostCategory(String postCategory) {
try {
return PostCategory.valueOf(postCategory);
} catch (IllegalArgumentException ex) {
throw new CustomException(ErrorCode.INVALID_POST_CATEGORY);
}
}

@Transactional(readOnly = true)
public List<BoardFindPostResponse> findPostsByCodeAndPostCategory(String code, String category) {

String boardCode = validateCode(code);
PostCategory postCategory = validatePostCategory(category);

Board board = boardRepository.getByCodeUsingEntityGraph(boardCode);
List<Post> postList = getPostListByPostCategory(board.getPostList(), postCategory);

return BoardFindPostResponse.from(postList);
}

private List<Post> getPostListByPostCategory(List<Post> postList, PostCategory postCategory) {
if (postCategory.equals(PostCategory.전체)) {
return postList;
}
return postList.stream()
.filter(post -> post.getCategory().equals(postCategory))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.solidconnection.comment.dto;

import com.example.solidconnection.entity.Comment;
import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse;

import java.time.LocalDateTime;

public record PostFindCommentResponse(
Long id,
Long parentId,
String content,
Boolean isOwner,
LocalDateTime createdAt,
LocalDateTime updatedAt,
PostFindSiteUserResponse postFindSiteUserResponse

) {
public static PostFindCommentResponse from(Boolean isOwner, Comment comment) {
return new PostFindCommentResponse(
comment.getId(),
getParentCommentId(comment),
comment.getContent(),
isOwner,
comment.getCreatedAt(),
comment.getUpdatedAt(),
PostFindSiteUserResponse.from(comment.getSiteUser())
);
}

private static Long getParentCommentId(Comment comment) {
if (comment.getParentComment() != null) {
return comment.getParentComment().getId();
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.solidconnection.comment.repository;

import com.example.solidconnection.entity.Comment;
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 CommentRepository extends JpaRepository<Comment, Long> {

@Query(value = """
WITH RECURSIVE CommentTree AS (
SELECT
id, parent_id, post_id, site_user_id, content,
created_at, updated_at,
0 AS level, CAST(id AS CHAR(255)) AS path
FROM comment
WHERE post_id = :postId AND parent_id IS NULL
UNION ALL
SELECT
c.id, c.parent_id, c.post_id, c.site_user_id, c.content,
c.created_at, c.updated_at,
ct.level + 1, CONCAT(ct.path, '->', c.id)
FROM comment c
INNER JOIN CommentTree ct ON c.parent_id = ct.id
)
SELECT * FROM CommentTree
ORDER BY path
""", nativeQuery = true)
List<Comment> findCommentTreeByPostId(@Param("postId") Long postId);
Comment on lines +12 to +31
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPQL 공부 열심히 하셨네요👍🔥


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.solidconnection.comment.service;

import com.example.solidconnection.comment.repository.CommentRepository;
import com.example.solidconnection.comment.dto.PostFindCommentResponse;
import com.example.solidconnection.entity.Comment;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class CommentService {

private final CommentRepository commentRepository;

private Boolean isOwner(Comment comment, String email) {
return comment.getSiteUser().getEmail().equals(email);
}


public List<PostFindCommentResponse> findCommentsByPostId(String email, Long postId) {
return commentRepository.findCommentTreeByPostId(postId)
.stream()
.map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment))
.collect(Collectors.toList());
}
}
Loading