Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d628f24
feat: notice 도메인 엔터티 생성 및 글 작성 api 개발
jwooo002 Aug 24, 2025
737e0b1
feat: 글 조회하기 api 개발
jwooo002 Aug 24, 2025
7516614
feat: 글 수정 api 개발
jwooo002 Aug 25, 2025
96dff53
feat: 글 삭제 api 개발
jwooo002 Aug 25, 2025
d1e12b6
feat: 글 상세보기 api 개발, 글 목록 조회 dto 수정
jwooo002 Aug 25, 2025
b9ba9da
fix: room 엔터티에 hostUserNo 저장되지 않는 문제 해결
jwooo002 Aug 25, 2025
e9d16c9
refactor: user 엔터티 room 필드 타입 변경
jwooo002 Aug 29, 2025
0ed0bba
feat: 댓글 작성 api 기능 개발
jwooo002 Sep 6, 2025
25cff7e
refactor: Notice 엔터티 필드 타입 변경에 따른 수정
jwooo002 Sep 6, 2025
0de56ce
Merge branch 'develop' into feat/#14/notice-기능-개발
jwooo002 Dec 24, 2025
8038500
Merge remote-tracking branch 'origin/develop' into feat/#14/notice-기능-개발
jwooo002 Dec 27, 2025
03ccee4
Merge remote-tracking branch 'origin/feat/#14/notice-기능-개발' into feat…
jwooo002 Dec 27, 2025
37f38f1
build: add s3 dependency
jwooo002 Dec 27, 2025
70862de
feat: add s3 config
jwooo002 Dec 27, 2025
1ed8068
feat: add generating s3 presigned url
jwooo002 Dec 27, 2025
9f90f91
feat: add image entity, repository, service
jwooo002 Dec 27, 2025
b997b81
feat: add image to notice create, update, delete, load
jwooo002 Dec 27, 2025
752077f
docs: add api spec
jwooo002 Dec 27, 2025
3999856
feat: add load comment
jwooo002 Dec 27, 2025
709336c
feat: develop update comment api
jwooo002 Dec 27, 2025
8453068
feat: develop delete comment api
jwooo002 Dec 27, 2025
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ dependencies {
// Firebase
implementation 'com.google.firebase:firebase-admin:9.2.0'

// AWS (v2 SDK BOM + S3)
implementation 'software.amazon.awssdk:s3:2.25.69'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.project.dorumdorum.domain.image.domain.entity;

import com.project.dorumdorum.global.common.BaseEntity;
import io.hypersistence.utils.hibernate.id.Tsid;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "image")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class Image extends BaseEntity {

@Id
@Tsid
private Long imageNo;

@Column(nullable = false)
private Long noticeNo;

@Column(nullable = false)
private String s3Key;

@Column(nullable = false)
private String fileName;

@Column(nullable = false)
private Long fileSize;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.project.dorumdorum.domain.image.domain.repository;

import com.project.dorumdorum.domain.image.domain.entity.Image;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface ImageRepository extends JpaRepository<Image, Long> {

Optional<Image> findByNoticeNo(Long noticeNo);

long deleteByNoticeNo(Long noticeNo);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.project.dorumdorum.domain.image.domain.service;

import com.project.dorumdorum.domain.image.domain.entity.Image;
import com.project.dorumdorum.domain.image.domain.repository.ImageRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class ImageService {

private final ImageRepository imageRepository;

@Transactional
public Image saveNoticeImage(Long noticeNo, String s3Key, String fileName, Long fileSize) {
Image image = Image.builder()
.noticeNo(noticeNo)
.s3Key(s3Key)
.fileName(fileName)
.fileSize(fileSize)
.build();
return imageRepository.save(image);
}

@Transactional(readOnly = true)
public Optional<Image> findByNoticeNo(Long noticeNo) {
return imageRepository.findByNoticeNo(noticeNo);
}

@Transactional
public void softDelete(Image image) {
image.delete();
imageRepository.save(image);
}

@Transactional
public void deleteByNoticeNo(Long noticeNo) {
imageRepository.deleteByNoticeNo(noticeNo);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.project.dorumdorum.domain.notice.application.dto.request;

public record UpdateCommentRequest(
Long commentNo,
String content
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.project.dorumdorum.domain.notice.application.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.lang.Nullable;

public record UpdateNoticeRequest(
@NotNull Long roomNo,
@NotNull Long noticeNo,
@NotBlank String title,
@NotBlank String content,
@Nullable String imageFileName,
@Nullable Long imageFileSize,
@Nullable Boolean deleteImage
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.project.dorumdorum.domain.notice.application.dto.request;

public record WriteCommentRequest(
Long noticeNo,
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.project.dorumdorum.domain.notice.application.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.lang.Nullable;

public record WriteNoticeRequest(
@NotNull Long roomNo,
@NotBlank String title,
@NotBlank String content,
@Nullable String imageFileName,
@Nullable Long imageFileSize
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.project.dorumdorum.domain.notice.application.dto.response;

import com.project.dorumdorum.domain.notice.domain.entity.Comment;
import lombok.Builder;

import java.time.LocalDateTime;

@Builder
public record CommentResponse(
Long commentNo,
Long userNo,
LocalDateTime updated_at,
String content
) {
public static CommentResponse create(Comment comment) {
return CommentResponse.builder()
.commentNo(comment.getCommentNo())
.userNo(comment.getUserNo())
.updated_at(comment.getUpdatedAt())
.content(comment.getContent())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.project.dorumdorum.domain.notice.application.dto.response;

import com.project.dorumdorum.domain.notice.domain.entity.Notice;

import lombok.Builder;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;

@Builder
public record NoticeResponse(
Long noticeNo,
Long userNo,
LocalDateTime createdAt,
LocalDateTime updatedAt,
String title,
String content,
String imageUploadUrl,
String imageDownloadUrl,
String imageFileName,
Long imageFileSize,
List<CommentResponse> comments
) {
public static NoticeResponse create(Notice notice) {
return create(notice, Collections.emptyList(), null, null, null, null);
}

public static NoticeResponse create(Notice notice, List<CommentResponse> comments) {
return create(notice, comments, null, null, null, null);
}

public static NoticeResponse create(Notice notice,
List<CommentResponse> comments,
String imageUploadUrl,
String imageDownloadUrl,
String imageFileName,
Long imageFileSize) {
return NoticeResponse.builder()
.noticeNo(notice.getNoticeNo())
.userNo(notice.getUserNo())
.createdAt(notice.getCreatedAt())
.updatedAt(notice.getUpdatedAt())
.title(notice.getTitle())
.content(notice.getContent())
.imageUploadUrl(imageUploadUrl)
.imageDownloadUrl(imageDownloadUrl)
.imageFileName(imageFileName)
.imageFileSize(imageFileSize)
.comments(comments == null ? Collections.emptyList() : comments)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.project.dorumdorum.domain.notice.application.dto.response;

import com.project.dorumdorum.domain.notice.domain.entity.Notice;
import lombok.Builder;

import java.time.LocalDateTime;

@Builder
public record NoticesResponse(
Long noticeNo,
Long userNo,
LocalDateTime updatedAt,
String title
) {
public static NoticesResponse create(Notice notice) {
return NoticesResponse.builder()
.noticeNo(notice.getNoticeNo())
.userNo(notice.getUserNo())
.updatedAt(notice.getUpdatedAt())
.title(notice.getTitle())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.project.dorumdorum.domain.notice.application.usecase;

import com.project.dorumdorum.domain.notice.domain.entity.Comment;
import com.project.dorumdorum.domain.notice.service.CommentService;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import com.project.dorumdorum.global.exception.code.status.GlobalErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class DeleteCommentUseCase {

private final UserService userService;
private final CommentService commentService;

@Transactional
public void execute(Long userNo, Long commentNo) {
userService.validateExistsById(userNo);

Comment comment = commentService.findActiveById(commentNo);

if (!comment.isWriter(userNo))
throw new RestApiException(GlobalErrorStatus.NO_PERMISSION_ON_NOTICE);

commentService.softDelete(comment);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.project.dorumdorum.domain.notice.application.usecase;

import com.project.dorumdorum.domain.image.domain.entity.Image;
import com.project.dorumdorum.domain.image.domain.service.ImageService;
import com.project.dorumdorum.domain.notice.domain.entity.Notice;
import com.project.dorumdorum.domain.notice.infra.S3PresignedUrlService;
import com.project.dorumdorum.domain.notice.service.NoticeService;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import com.project.dorumdorum.global.exception.code.status.GlobalErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class DeleteNoticeUseCase {

private final UserService userService;
private final NoticeService noticeService;
private final ImageService imageService;
private final S3PresignedUrlService s3PresignedUrlService;

@Transactional
public void execute(Long userNo, Long noticeNo) {
userService.validateExistsById(userNo);

Notice notice = noticeService.findById(noticeNo);

if (notice.isDeleted())
throw new RestApiException(GlobalErrorStatus.NOTICE_ALREADY_DELETED);
if (!notice.isWriter(userNo))
throw new RestApiException(GlobalErrorStatus.NO_PERMISSION_ON_NOTICE);

Image image = imageService.findByNoticeNo(noticeNo).orElse(null);
if (image != null) {
s3PresignedUrlService.deleteObject(image.getS3Key()); // 버킷에는 하드 삭제
imageService.softDelete(image); // image는 소프트 삭제
}

noticeService.softDelete(notice); // notice 소프트 삭제
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.project.dorumdorum.domain.notice.application.usecase;

import com.project.dorumdorum.domain.image.domain.entity.Image;
import com.project.dorumdorum.domain.image.domain.service.ImageService;
import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse;
import com.project.dorumdorum.domain.notice.application.dto.response.NoticesResponse;
import com.project.dorumdorum.domain.notice.application.dto.response.CommentResponse;
import com.project.dorumdorum.domain.notice.domain.entity.Notice;
import com.project.dorumdorum.domain.notice.infra.S3PresignedUrlService;
import com.project.dorumdorum.domain.notice.service.NoticeService;
import com.project.dorumdorum.domain.notice.service.CommentService;
import com.project.dorumdorum.domain.room.domain.entity.Room;
import com.project.dorumdorum.domain.room.domain.service.RoomService;
import com.project.dorumdorum.domain.room.domain.service.RoommateService;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import com.project.dorumdorum.global.exception.code.status.GlobalErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class LoadNoticesUseCase {

private final UserService userService;
private final RoommateService roommateService;
private final RoomService roomService;
private final NoticeService noticeService;
private final ImageService imageService;
private final S3PresignedUrlService s3PresignedUrlService;
private final CommentService commentService;

public List<NoticesResponse> loadNotices(Long userNo, Long roomNo) {
userService.validateExistsById(userNo);

Room room = roomService.findById(roomNo);
if(!roommateService.isUserInRoom(userNo, room))
throw new RestApiException(GlobalErrorStatus.USER_NOT_IN_ROOM);

return noticeService.loadNoticeList(room.getRoomNo());
}

public NoticeResponse loadNotice(Long userNo, Long noticeNo) {
userService.validateExistsById(userNo);

Notice notice = noticeService.findById(noticeNo);
Image image = imageService.findByNoticeNo(noticeNo).orElse(null);
List<CommentResponse> comments = commentService.findByNoticeNo(noticeNo).stream()
.map(CommentResponse::create)
.toList();

String downloadUrl = null;
String fileName = null;
Long fileSize = null;

if (image != null) {
downloadUrl = s3PresignedUrlService.generateDownloadPresignedUrl(image.getS3Key());
fileName = image.getFileName();
fileSize = image.getFileSize();
}

return NoticeResponse.create(notice, comments, null, downloadUrl, fileName, fileSize);
}
}
Loading