본문 바로가기
Projects/청하-청년을 위한 커뮤니티 서비스

[청하] 8. 게시글 상세조회 기능 구현 (feat. LocalDateTime)

by Lpromotion 2024. 10. 7.

게시글 등록 기능에 이어서 추가하는 기능은 “게시글 상세조회” 이다.

 

1. 프로젝트 구조

src/main/java/com/example/withpeace/
│
├── domain/                 # 도메인 모델 (엔티티)
│   ├── Image.java          # 이미지 엔티티
│   └── Post.java           # 게시글 엔티티
│
├── repository/             # 데이터 접근 계층
│   ├── UserRepository.java    # 사용자 레포지토리
│   ├── ImageRepository.java    # 이미지 레포지토리
│   └── PostRepository.java     # 게시글 레포지토리
│
├── dto/                    # 데이터 전송 객체
│   └── response/
│       └── PostDetailResponseDto.java # 게시글 상세조회 응답 DTO
│
├── controller/             # 컨트롤러 계층
│   └── PostController.java # 게시글 관련 API 엔드포인트
│
├── service/                # 비즈니스 로직 계층
│   └── PostService.java    # 게시글 관련 비즈니스 로직
│
└── util/                   # 유틸리티 클래스
    └── TimeFormatter.java  # 시간 포맷팅 유틸리티

 

2. Post 엔티티 수정

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicUpdate
@Table(name = "posts")
public class Post {
    // ... (다른 필드 생략)

    @Column(name = "create_date", nullable = false)
    // private LocalDate createDate; // 변경 전
    private LocalDateTime createDate; // 변경 후

    @Builder
    public Post(User writer, String title, String content, ETopic type) {
        this.writer = writer;
        this.title = title;
        this.content = content;
        this.type = type;
        // this.createDate = LocalDate.now(); // 변경 전
        this.createDate = LocalDateTime.now(); // 변경 후
    }

}

createDate 필드의 타입을 LocalDate에서 LocalDateTime으로 변경했다. 게시글 업로드 시간을 정확하게 기록하고, 상세조회 시 시간 포맷팅을 위해 변경했다.

 

LocalDate VS LocalDateTime

  • LocalDate
    • 날짜만 저장한다. (년, 월, 일)
    • ex) “2024-02-15”
  • LocalDateTime
    • 날짜와 시간을 저장한다. (년, 월, 일, 시, 분, 초, 나노초)
    • ex) "2024-02-15T14:00”
    • 시간 기반 연산과 비교가 가능하다.

 

3. DTO 구현

@Builder
public record PostDetailResponseDto(
        Long postId,
        Long userId,
        String nickname,
        String profileImageUrl,
        String title,
        String content,
        ETopic type,
        String createDate,
        List<String> postImageUrls) {
}

`PostDetailResponseDto` 는 게시글 상세조회의 응답을 위한 데이터 전송 객체이다.

해당 게시글을 작성한 사용자의 정보와 게시글의 정보, 게시글 관련 이미지 URL을 포함한다.

 

4. 레포지토리 구현

@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {

    @Query("SELECT i.url FROM Image i WHERE i.post = :post")
    List<String> findUrlsByPost(Post post);

}

`findUrlsByPost()` 는 특정 게시글과 연관된 모든 이미지 URL을 조회하는 커스텀 쿼리 메서드이다.

 

5. 컨트롤러 구현

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/posts")
public class PostController {
    private final PostService postService;
    // ... (게시글 등록 관련 메서드 생략)

    // 게시글 상세조회
    @GetMapping("/{postId}")
    public ResponseDto<PostDetailResponseDto> getPostDetail(@UserId Long userId, @PathVariable Long postId) {
        return ResponseDto.ok(postService.getPostDetail(userId, postId));
    }
}

인증된 사용자 ID와 경로변수로 게시글 ID를 받아 서비스 계층에 넘긴다.

 

6. 서비스 구현

@Service
@RequiredArgsConstructor
public class PostService {
    // ... (필드 선언 생략)
    // ... (게시글 등록 관련 메서드 생략)

    @Transactional
    public PostDetailResponseDto getPostDetail(Long userId, Long postId) {
        User user =
                userRepository.findById(userId).orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER));
        Post post =
                postRepository.findById(postId).orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_POST));

        String formatterDate = TimeFormatter.timeFormat(post.getCreateDate());

        List<String> postImageUrls = Optional.ofNullable(imageRepository.findUrlsByPost(post))
                                                .orElse(Collections.emptyList());

        PostDetailResponseDto postDetailResponseDto =
                PostDetailResponseDto.builder()
                        .postId(postId)
                        .userId(post.getWriter().getId())
                        .nickname(user.getNickname())
                        .profileImageUrl(user.getProfileImage())
                        .title(post.getTitle())
                        .content(post.getContent())
                        .type(post.getType())
                        .createDate(formatterDate)
                        .postImageUrls(postImageUrls)
                        .build();

        return postDetailResponseDto;
    }
}

사용자와 게시글 정보를 조회하고, 만약 찾지 못한 경우 에러를 반환한다.

게시글 생성 시간(createDate)를 포맷팅하고, 연관 이미지 URL을 가져와 DTO를 구성한다.

 

List<String> postImageUrls = Optional.ofNullable(imageRepository.findUrlsByPostId(post))
                                                .orElse(Collections.emptyList());

연관 이미지 URL이 없으면 빈 리스트를 생성한다.

 

7. 유틸리티 클래스 구현

@Component
public class TimeFormatter {

    public static String timeFormat(LocalDateTime time) {
        LocalDateTime now = LocalDateTime.now();
        Duration duration = Duration.between(time, now);

        if(duration.getSeconds() < 60) {
            return duration.getSeconds() + "초 전";
        } else if(duration.toMinutes() < 60) {
            return duration.toMinutes() + "분 전";
        } else if(duration.toHours() < 24) {
            return duration.toHours() + "시간 전";
        } else if(duration.toDays() < 7) {
            return duration.toDays() + "일 전";
        } else {
            if (time.getYear() != now.getYear()) {
                return time.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일"));
            } else {
                return time.format(DateTimeFormatter.ofPattern("M월 d일"));
            }
        }
    }
}

TimeFormatter는 게시글 생성 시간(createDate)과 현재 시간의 차이에 따라 적절한 형식의 문자열을 반환한다.

  • 1분 미만: “~초 전”
  • 1시간 미만: “~분 전”
  • 24시간 미만: “~시간 전”
  • 7일 미만: “~일 전”
  • 올해 아님: "yyyy년 M월 d일"
  • 올해: "M월 d일"
반응형

댓글