Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation 'org.springframework.boot:spring-boot-starter-validation' // 유효성 검사 위해
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sopt.univoice.domain.notice.controller;


import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -20,7 +21,7 @@ public class NoticeController {
private final NoticeService noticeService;

@PostMapping("/create")
public ResponseEntity<SuccessStatusResponse<Void>> createPost(@ModelAttribute NoticeCreateRequest noticeCreateRequest) {
public ResponseEntity<SuccessStatusResponse<Void>> createPost(@Valid @ModelAttribute NoticeCreateRequest noticeCreateRequest) {
System.out.println("createPost method called with request: " + noticeCreateRequest);
noticeService.createPost(noticeCreateRequest);
return ResponseEntity.status(HttpStatus.CREATED)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package sopt.univoice.domain.notice.dto;

import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
Expand All @@ -15,7 +16,7 @@
//@AllArgsConstructor
public class NoticeCreateRequest {


@Size(max = 100)
private String title;

private String content;
Expand All @@ -27,6 +28,6 @@ public class NoticeCreateRequest {
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime endTime;

private List<MultipartFile> studentCardImages;

@Size(max = 5)
private List<MultipartFile> imageList;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@
@AllArgsConstructor
public class NoticeDetailResponseDTO {
private Long id;
private String writeAffiliation;
private String title;
private String content;
private Long noticeLike;
private Long viewCount;
private String target;
private LocalDateTime startTime;
private LocalDateTime endTime;
private String category;
private String contentSummary;
private Long memberId;
private String writeAffiliation;
private String content;
private LocalDateTime createdAt;
private Long viewCount;
private List<String> noticeImages;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ public class NoticeSaveDTO {
private Long viewCount;
private Long noticeLike;
private String category;
private LocalDateTime startTime;
private LocalDateTime endTime;
private LocalDateTime createdAt;
private String image;
}
8 changes: 8 additions & 0 deletions src/main/java/sopt/univoice/domain/notice/entity/Notice.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ public class Notice extends BaseTimeEntity {
private Long noticeLike = 0L;

private Long viewCount = 0L;

private Long noticeSave = 0L;

private String target;


private LocalDateTime startTime;
private LocalDateTime endTime;

Expand Down Expand Up @@ -71,6 +75,7 @@ public Notice(String title, String content, String target, LocalDateTime startTi
this.member = member;
this.noticeLike = 0L;
this.viewCount = 0L;
this.noticeSave = 0L;
this.contentSummary = contentSummary;
this.category = category;
}
Expand All @@ -89,6 +94,9 @@ public Long getViewCount() {
return viewCount;
}

public void setNoticeSave(Long noticeSave) {
this.noticeSave = noticeSave;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import sopt.univoice.domain.user.entity.Member;
import sopt.univoice.infra.persistence.BaseTimeEntity;


@Entity
@Getter
@NoArgsConstructor
public class SaveNotice {
public class SaveNotice extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@

public interface NoticeLikeRepository extends JpaRepository<NoticeLike, Long> {
Optional<NoticeLike> findByNoticeAndMember(Notice notice, Member member);

boolean existsByNoticeAndMember(Notice notice, Member member);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
public interface SaveNoticeRepository extends JpaRepository<SaveNotice, Long>{
Optional<SaveNotice> findByNoticeAndMember(Notice notice, Member member);
List<SaveNotice> findByMember(Member member);

boolean existsByNoticeAndMember(Notice notice, Member member);
}


Expand Down
124 changes: 77 additions & 47 deletions src/main/java/sopt/univoice/domain/notice/service/NoticeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import sopt.univoice.domain.user.entity.Member;
import sopt.univoice.domain.affiliation.entity.Role;
import sopt.univoice.infra.common.exception.UnauthorizedException;
import sopt.univoice.infra.common.exception.message.BusinessException;
import sopt.univoice.infra.common.exception.message.ErrorMessage;
import sopt.univoice.infra.external.OpenAiService;
import sopt.univoice.infra.external.S3Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -32,16 +34,16 @@ public class NoticeService {
private final PrincipalHandler principalHandler;
private final S3Service s3Service;
private final OpenAiService openAiService;
private final NoticeViewRepository noticeViewRepository;
private final NoticeLikeRepository noticeLikeRepository;
private final SaveNoticeRepository saveNoticeRepository;
private final NoticeViewRepository noticeViewRepository;

@Transactional
public void createPost(NoticeCreateRequest noticeCreateRequest) {
Long memberId = principalHandler.getUserIdFromPrincipal();
System.out.println("Authenticated Member ID: " + memberId);
Member member = authRepository.findById(memberId)
.orElseThrow(() -> new RuntimeException("회원이 존재하지 않습니다."));
.orElseThrow(() -> new RuntimeException("회원이 존재하지 않습니다."));
System.out.println("Member Role: " + member.getAffiliation().getRole());

String summarizedContent = null;
Expand All @@ -61,27 +63,36 @@ public void createPost(NoticeCreateRequest noticeCreateRequest) {

// Notice 엔티티 생성 및 저장
Notice notice = Notice.builder()
.title(noticeCreateRequest.getTitle())
.content(noticeCreateRequest.getContent())
.target(noticeCreateRequest.getTarget())
.startTime(noticeCreateRequest.getStartTime())
.endTime(noticeCreateRequest.getEndTime())
.member(member)
.contentSummary(summarizedContent)
.category("공지사항") // category 값을 '공지사항'으로 설정
.build();
.title(noticeCreateRequest.getTitle())
.content(noticeCreateRequest.getContent())
.target(noticeCreateRequest.getTarget() != null ? noticeCreateRequest.getTarget() : "")
.startTime(noticeCreateRequest.getStartTime() != null ? noticeCreateRequest.getStartTime() : null)
.endTime(noticeCreateRequest.getEndTime() != null ? noticeCreateRequest.getEndTime() : null)
.member(member)
.contentSummary(summarizedContent)
.category("공지사항") // category 값을 '공지사항'으로 설정
.build();
noticeRepository.save(notice);
System.out.println("Notice saved successfully with ID: " + notice.getId());

// NoticeImage 엔티티 생성 및 저장
for (MultipartFile file : noticeCreateRequest.getStudentCardImages()) {
String fileName = storeFile(file); // 파일 저장 로직 필요
NoticeImage noticeImage = NoticeImage.builder()
.notice(notice)
.noticeImage(fileName)
.build();
noticeImageRepository.save(noticeImage);
System.out.println("NoticeImage saved successfully with file name: " + fileName);
List<MultipartFile> files = noticeCreateRequest.getImageList();

if (files != null && !files.isEmpty()) {
for (MultipartFile file : files) {
try {
String fileUrl = storeFile(file); // 파일 저장 로직 필요
NoticeImage noticeImage = NoticeImage.builder()
.notice(notice)
.noticeImage(fileUrl)
.build();
noticeImageRepository.save(noticeImage);
System.out.println("NoticeImage saved successfully with file URL: " + fileUrl);
} catch (Exception e) {
System.err.println("Error saving NoticeImage: " + e.getMessage());
e.printStackTrace();
}
}
}

// NoticeView 엔티티 생성 및 저장
Expand All @@ -90,24 +101,24 @@ public void createPost(NoticeCreateRequest noticeCreateRequest) {

for (Member universityMember : universityMembers) {
NoticeView noticeView = NoticeView.builder()
.notice(notice)
.member(universityMember)
.readAt(false)
.build();
.notice(notice)
.member(universityMember)
.readAt(false)
.build();
noticeViewRepository.save(noticeView);
}

}

private String storeFile(MultipartFile file) {
try {
return s3Service.uploadImage("notice-images/", file);
String fileUrl = s3Service.uploadImage("notice-images/", file);
return fileUrl;
} catch (IOException e) {
System.err.println("File upload failed: " + e.getMessage());
throw new RuntimeException("파일 업로드에 실패했습니다.", e);
}
}


@Transactional
public void likeNotice(Long noticeId) {
Long memberId = principalHandler.getUserIdFromPrincipal();
Expand All @@ -118,6 +129,12 @@ public void likeNotice(Long noticeId) {
Notice notice = noticeRepository.findById(noticeId)
.orElseThrow(() -> new RuntimeException("공지사항이 존재하지 않습니다."));

// 이미 좋아요를 누른 것인지 확인
boolean alreadyLiked = noticeLikeRepository.existsByNoticeAndMember(notice, member);
if (alreadyLiked) {
throw new BusinessException(ErrorMessage.ALREADY_LIKED);
}

// noticeLike를 1 증가시킵니다.
notice.setNoticeLike(notice.getNoticeLike() + 1);
noticeRepository.save(notice);
Expand Down Expand Up @@ -161,6 +178,16 @@ public void saveNotice(Long noticeId) {
Notice notice = noticeRepository.findById(noticeId)
.orElseThrow(() -> new RuntimeException("공지사항이 존재하지 않습니다."));

// 이미 저장한 공지인지 확인하는 부분 추가
boolean alreadySaved = saveNoticeRepository.existsByNoticeAndMember(notice, member);
if (alreadySaved) {
throw new BusinessException(ErrorMessage.ALREADY_SAVED);
}

// noticeSave를 1 증가시킵니다.
notice.setNoticeSave(notice.getNoticeSave() + 1);
noticeRepository.save(notice);

// SaveNotice 엔티티 생성 및 저장
SaveNotice saveNotice = SaveNotice.builder()
.notice(notice)
Expand All @@ -179,9 +206,13 @@ public void saveCancleNotice(Long noticeId) {
Notice notice = noticeRepository.findById(noticeId)
.orElseThrow(() -> new RuntimeException("공지사항이 존재하지 않습니다."));

// noticeSave를 1 감소시킵니다.
notice.setNoticeSave(notice.getNoticeSave() - 1);
noticeRepository.save(notice);

// SaveNotice 엔티티 삭제
SaveNotice saveNotice = saveNoticeRepository.findByNoticeAndMember(notice, member)
.orElseThrow(() -> new RuntimeException("저장된 공지사항이 존재하지 않습니다."));
.orElseThrow(() -> new RuntimeException("저장된 공지사항이 존재하지 않습니다."));
saveNoticeRepository.delete(saveNotice);
}

Expand All @@ -195,19 +226,21 @@ public List<NoticeSaveDTO> getSaveNoticeByUser() {
List<SaveNotice> saveNotices = saveNoticeRepository.findByMember(member);

return saveNotices.stream()
.map(saveNotice -> {
Notice notice = saveNotice.getNotice();
return new NoticeSaveDTO(
notice.getId(),
notice.getTitle(),
notice.getViewCount(),
notice.getNoticeLike(),
notice.getCategory(),
notice.getStartTime(),
notice.getEndTime()
);
})
.collect(Collectors.toList());
.map(saveNotice -> {
Notice notice = saveNotice.getNotice();
String image = notice.getNoticeImages().isEmpty() ? null : notice.getNoticeImages().get(0).getNoticeImage();
return new NoticeSaveDTO(
notice.getId(),
notice.getTitle(),
notice.getViewCount(),
notice.getNoticeLike(),
notice.getCategory(),
notice.getCreatedAt(),
image
);
})
.sorted(Comparator.comparing(NoticeSaveDTO::getCreatedAt))
.collect(Collectors.toList());
}

@Transactional
Expand Down Expand Up @@ -463,17 +496,14 @@ public NoticeDetailResponseDTO getNoticeById(Long noticeId) {

return new NoticeDetailResponseDTO(
notice.getId(),
writeAffiliation,
notice.getTitle(),
notice.getContent(),
notice.getNoticeLike(),
notice.getViewCount(),
notice.getTarget(),
notice.getStartTime(),
notice.getEndTime(),
notice.getCategory(),
notice.getContentSummary(),
notice.getMember().getId(),
writeAffiliation, // 추가된 부분
notice.getContent(),
notice.getCreatedAt(),
notice.getViewCount(),
notice.getNoticeImages().stream().map(NoticeImage::getNoticeImage).collect(Collectors.toList())
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package sopt.univoice.infra.common.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class ValidationExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public enum ErrorMessage {
EMAIL_DUPLICATE(HttpStatus.NO_CONTENT.value(), "이미 사용중인 이메일 입니다."),
JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."),
APPROVEADMIN_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "승인된 관리자의 로그인 검증을 실패했습니다."),
ALREADY_SAVED(HttpStatus.CONFLICT.value(), "이미 저장한 공지입니다."),
ALREADY_LIKED(HttpStatus.CONFLICT.value(), "이미 좋아요한 공지입니다."),
;
private final int status;
private final String message;
Expand Down
Loading