Skip to content

Commit

Permalink
Merge pull request #46 from Move-Log/feat/news-create
Browse files Browse the repository at this point in the history
[FEAT] 뉴스 저장 기능 구현
  • Loading branch information
EunbeenDev authored Jan 13, 2025
2 parents 85a058c + b855b68 commit 6356304
Show file tree
Hide file tree
Showing 21 changed files with 329 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
@Service
@RequiredArgsConstructor
@Slf4j
public class HeadlineGeneratorService {
public class HeadLineGeneratorService {

private final GptService gptService;

Expand Down
59 changes: 50 additions & 9 deletions src/main/java/com/movelog/domain/news/application/NewsService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package com.movelog.domain.news.application;

import com.movelog.domain.news.domain.News;
import com.movelog.domain.news.domain.repository.NewsRepository;
import com.movelog.domain.news.dto.request.CreateNewsReq;
import com.movelog.domain.news.dto.request.NewsHeadLineReq;
import com.movelog.domain.news.dto.response.HeadLineRes;
import com.movelog.domain.record.domain.Keyword;
import com.movelog.domain.record.domain.VerbType;
import com.movelog.domain.record.exception.KeywordNotFoundException;
import com.movelog.domain.record.repository.KeywordRepository;
import com.movelog.domain.user.application.UserService;
import com.movelog.domain.user.domain.User;
import com.movelog.domain.user.domain.repository.UserRepository;
import com.movelog.domain.user.exception.UserNotFoundException;
import com.movelog.global.DefaultAssert;
import com.movelog.global.config.security.token.UserPrincipal;
import com.movelog.global.util.S3Util;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Optional;
Expand All @@ -20,24 +27,58 @@
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NewsService {
private final HeadlineGeneratorService headlineGeneratorService;
private final HeadLineGeneratorService headLineGeneratorService;
private final UserService userService;
private final UserRepository userRepository;
private final KeywordRepository keywordRepository;
private final NewsRepository newsRepository;
private final S3Util s3Util;

public List<HeadLineRes> createHeadLine(UserPrincipal userPrincipal, NewsHeadLineReq newsHeadLineReq) {
public List<HeadLineRes> createHeadLine(UserPrincipal userPrincipal, Long keywordId, NewsHeadLineReq newsHeadLineReq) {
User user = validateUser(userPrincipal);
// id가 5인 유저 정보(테스트용)
// User user = userRepository.findById(5L).orElseThrow(UserNotFoundException::new);
Keyword keyword = validateKeyword(keywordId);
String option = newsHeadLineReq.getOption();
String verb = newsHeadLineReq.getVerb();
String noun = newsHeadLineReq.getNoun();
return headlineGeneratorService.generateHeadLine(option, verb, noun);
String verb = VerbType.getStringVerbType(keyword.getVerbType());
String noun = keyword.getKeyword();

return headLineGeneratorService.generateHeadLine(option, verb, noun);
}

@Transactional
public void createNews(UserPrincipal userPrincipal, Long keywordId, CreateNewsReq createNewsReq, MultipartFile img) {
User user = validateUser(userPrincipal);
// id가 5인 유저 정보(테스트용)
// User user = userRepository.findById(5L).orElseThrow(UserNotFoundException::new);
Keyword keyword = validateKeyword(keywordId);

String newsImgUrl = s3Util.uploadToNewsFolder(img);

News news = News.builder()
.headLine(createNewsReq.getHeadLine())
.newsUrl(newsImgUrl)
.keyword(keyword)
.build();

newsRepository.save(news);

}



// User 정보 검증
private User validateUser(UserPrincipal userPrincipal) {
Optional<User> userOptional = userService.findById(userPrincipal.getId());
DefaultAssert.isOptionalPresent(userOptional);
if (userOptional.isEmpty()) { throw new UserNotFoundException(); }
return userOptional.get();
}

// Keyword 정보 검증
private Keyword validateKeyword(Long keywordId) {
Optional<Keyword> keywordOptional = keywordRepository.findById(keywordId);
if (keywordOptional.isEmpty()) { throw new KeywordNotFoundException(); }
return keywordOptional.get();
}


}
38 changes: 38 additions & 0 deletions src/main/java/com/movelog/domain/news/domain/News.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.movelog.domain.news.domain;

import com.movelog.domain.common.BaseEntity;
import com.movelog.domain.record.domain.Keyword;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "News")
@NoArgsConstructor
@Getter
public class News extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "news_id", updatable = false)
private Long newsId;

private String headLine; //뉴스 헤드라인

private String newsUrl; //뉴스 URL

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "keyword_id")
private Keyword keyword;

@Builder
public News(String headLine, String newsUrl, Keyword keyword) {
this.headLine = headLine;
this.newsUrl = newsUrl;
this.keyword = keyword;
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.movelog.domain.news.domain.repository;

import com.movelog.domain.news.domain.News;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NewsRepository extends JpaRepository<News, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.movelog.domain.news.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class CreateNewsReq {

@Schema( type = "String", example ="5년 만의 첫 도전, 무엇이 그를 움직이게 했나?", description="사용자가 선택한/생성한 헤드라인 정보입니다.")
private String headLine;

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,4 @@
public class NewsHeadLineReq {
@Schema( type = "String", example ="첫 도전, 오랜만에 다시, 꾸준히 이어온 기록, 끊어낸 습관 중 택 1", description="뉴스 헤드라인 고정 옵션입니다.")
private String option;

@Schema( type = "String", example ="했어요, 먹었어요, 갔어요 중 택 1", description="사용자가 선택한 동사 정보입니다.")
private String verb;

@Schema( type = "String", example ="클라이밍", description="사용자가 선택한 명사 정보입니다.")
private String noun;


}
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package com.movelog.domain.news.presentation;

import com.movelog.domain.news.application.NewsService;
import com.movelog.domain.news.dto.request.CreateNewsReq;
import com.movelog.domain.news.dto.request.NewsHeadLineReq;
import com.movelog.domain.news.dto.response.HeadLineRes;
import com.movelog.global.config.security.token.CurrentUser;
import com.movelog.global.config.security.token.UserPrincipal;
import com.movelog.global.payload.Message;
import com.movelog.global.util.ApiResponseUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand All @@ -30,17 +37,43 @@ public class NewsController {

@Operation(summary = "뉴스 헤드라인 생성 API", description = "뉴스 헤드라인을 생성하는 API입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "뉴스 헤드라인 생성 성공"),
@ApiResponse(responseCode = "400", description = "뉴스 헤드라인 생성 실패")
@ApiResponse(responseCode = "200", description = "뉴스 헤드라인 생성 성공",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "400", description = "뉴스 헤드라인 생성 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/headline")
public List<HeadLineRes> createHeadLine(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@PostMapping("/{keywordId}/headline")
public ResponseEntity<?> createHeadLine(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
@Parameter(description = "키워드 ID(동사-명사 쌍에 대한 ID)를 입력해주세요.", required = true) @PathVariable Long keywordId,
@Parameter(description = "뉴스 헤드라인 생성 요청", required = true) @RequestBody NewsHeadLineReq newsHeadLineReq
) {
return newsService.createHeadLine(userPrincipal, newsHeadLineReq);
List<HeadLineRes> response = newsService.createHeadLine(userPrincipal, keywordId, newsHeadLineReq);
return ResponseEntity.ok(ApiResponseUtil.success(response));
}


@Operation(summary = "뉴스 생성 및 저장 API(기존 이미지 기록 기반)", description = "사용자의 기존 기록 이미지로 뉴스를 생성합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "뉴스 생성 및 저장 성공",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))),
@ApiResponse(responseCode = "400", description = "뉴스 생성 및 저장 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/{keywordId}")
public ResponseEntity<?> createNews(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
@Parameter(description = "키워드 ID(동사-명사 쌍에 대한 ID)를 입력해주세요.", required = true) @PathVariable Long keywordId,
@Parameter(description = "뉴스 생성 및 저장을 위한 정보를 입력해주세요", required = true) @RequestPart CreateNewsReq createNewsReq,
@Parameter(description = "뉴스 이미지를 파일 형식으로 입력해주세요", required = true) @RequestParam(value = "img", required = false) MultipartFile img
) {
newsService.createNews(userPrincipal, keywordId, createNewsReq, img);
return ResponseEntity.ok(ApiResponseUtil.success(Message.builder().message("뉴스가 생성되었습니다.").build()));
}






}
19 changes: 17 additions & 2 deletions src/main/java/com/movelog/domain/record/domain/Keyword.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.movelog.domain.record.domain;

import com.movelog.domain.common.BaseEntity;
import com.movelog.domain.news.domain.News;
import com.movelog.domain.user.domain.User;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -20,13 +22,26 @@ public class Keyword extends BaseEntity {
@Column(name = "keyword_id", updatable = false)
private Long keywordId;

private String keyword;
private String keyword; //명사

@Enumerated(EnumType.STRING) // Enum을 문자열로 저장
@Column(name = "verb_type")
private VerbType verbType;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@OneToMany(mappedBy = "keyword")
private List<Record> records = new ArrayList<>();

@OneToMany(mappedBy = "keyword")
private List<News> news = new ArrayList<>();

@Builder
public Keyword(String keyword) {
public Keyword(User user, String keyword, VerbType verbType) {
this.user = user;
this.keyword = keyword;
this.verbType = verbType;
}
}
11 changes: 1 addition & 10 deletions src/main/java/com/movelog/domain/record/domain/Record.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,19 @@ public class Record extends BaseEntity {
@Column(name = "record_id", updatable = false)
private Long recordId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "keyword_id")
private Keyword keyword;

@Enumerated(EnumType.STRING) // Enum을 문자열로 저장
private VerbType verbType;

@Column(name = "record_image")
private String recordImage;

@Column(name = "action_time")
private java.time.LocalDateTime actionTime;

@Builder
public Record(User user, Keyword keyword, VerbType verbType, String recordImage) {
this.user = user;
public Record(Keyword keyword, String recordImage) {
this.keyword = keyword;
this.verbType = verbType;
this.recordImage = recordImage;
this.actionTime = actionTime == null? LocalDateTime.now():actionTime;
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/movelog/domain/record/domain/VerbType.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ public static VerbType fromValue(String value) {
}
throw new IllegalArgumentException("No enum constant for value: " + value);
}

public static String getStringVerbType(VerbType verbType) {
return verbType.getVerbType();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.movelog.domain.record.exception;


import com.movelog.global.exception.NotFoundException;

public class KeywordNotFoundException extends NotFoundException {

public KeywordNotFoundException() {
super("U002", "키워드를 찾을 수 없습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public ResponseEntity<ApiResponse> retrieveTodayRecord(

ApiResponse result = ApiResponse.builder()
.check(true)
.information(recordService.retrieveTodayRecord(userPrincipal.getId()))
.information(recordService.retrieveTodayRecord(5L))
.build();
return ResponseEntity.ok(result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import com.movelog.domain.record.domain.Keyword;
import com.movelog.domain.record.domain.Record;
import com.movelog.domain.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface KeywordRepository extends JpaRepository<Keyword,Long> {
List<Keyword> findByUser(User user);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.movelog.domain.record.repository;

import com.movelog.domain.record.domain.Keyword;
import com.movelog.domain.record.domain.Record;
import com.movelog.domain.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -10,5 +11,5 @@

@Repository
public interface RecordRepository extends JpaRepository<Record,Long> {
List<Record> findByUserAndActionTimeBetween(User user, LocalDateTime startOfDay, LocalDateTime endOfDay);
List<Record> findByKeywordInAndActionTimeBetween(List<Keyword> keywords, LocalDateTime startTime, LocalDateTime endTime);
}
Loading

0 comments on commit 6356304

Please sign in to comment.