From 7854fa3671aa8eb309f80015969d3c01a8710632 Mon Sep 17 00:00:00 2001 From: myqewr Date: Fri, 4 Apr 2025 21:33:06 +0900 Subject: [PATCH] [feat] : Add "Create New Diary For Greenroom" API --- server/src/docs/asciidoc/index.adoc | 37 +++++ .../controller/GreenroomController.java | 8 +- .../dto/in/DiaryCreationRequestDto.java | 19 +++ .../greenroom/dto/out/DiaryResponseDto.java | 25 +++ .../api/domain/greenroom/entity/Diary.java | 21 ++- .../greenroom/service/DiaryService.java | 19 ++- .../greenroom/service/GreenroomService.java | 16 ++ .../server/api/utils/S3ImageUploader.java | 7 + server/src/main/resources/application-dev.yml | 1 + .../src/main/resources/application-local.yml | 1 + .../server/api/GreenroomIntegrationTest.java | 147 ++++++++++++++++++ server/src/test/resources/schema-test.sql | 1 + 12 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/in/DiaryCreationRequestDto.java create mode 100644 server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/out/DiaryResponseDto.java diff --git a/server/src/docs/asciidoc/index.adoc b/server/src/docs/asciidoc/index.adoc index 92fbac1..399404e 100644 --- a/server/src/docs/asciidoc/index.adoc +++ b/server/src/docs/asciidoc/index.adoc @@ -1077,3 +1077,40 @@ include::{snippets}/api/greenroom/calendar/1/http-response.adoc[] ==== Response Body Fields include::{snippets}/api/greenroom/calendar/1/response-fields.adoc[] + + +=== **15. 일기 등록** + +사용자가 현재 가지고 있는 활성화된 그린룸에 대해서 일기를 작성함 + +==== Request +include::{snippets}/api/greenroom/diary/post/1/curl-request.adoc[] + +==== Request Headers +include::{snippets}/api/greenroom/diary/post/1/request-headers.adoc[] + +==== Request Parts +이미지 파일은 선택 요소입니다.(필수x) +include::{snippets}/api/greenroom/diary/post/1/request-parts.adoc[] + +==== Request Parts : **data** - Detail Fields +include::{snippets}/api/greenroom/diary/post/1/request-part-data-fields.adoc[] + +==== 성공 Response +include::{snippets}/api/greenroom/diary/post/1/http-response.adoc[] + +==== Response Body Fields +include::{snippets}/api/greenroom/diary/post/1/response-fields.adoc[] + + +==== 실패 Response +실패 1. +include::{snippets}/api/greenroom/diary/post/2/http-response.adoc[] + +실패 2. +include::{snippets}/api/greenroom/diary/post/3/http-response.adoc[] + +실패 3. +include::{snippets}/api/greenroom/diary/post/4/http-response.adoc[] + + diff --git a/server/src/main/java/com/greenroom/server/api/domain/greenroom/controller/GreenroomController.java b/server/src/main/java/com/greenroom/server/api/domain/greenroom/controller/GreenroomController.java index 5ee4d8b..c064661 100644 --- a/server/src/main/java/com/greenroom/server/api/domain/greenroom/controller/GreenroomController.java +++ b/server/src/main/java/com/greenroom/server/api/domain/greenroom/controller/GreenroomController.java @@ -1,6 +1,5 @@ package com.greenroom.server.api.domain.greenroom.controller; -import com.google.protobuf.Api; import com.greenroom.server.api.domain.greenroom.dto.in.*; import com.greenroom.server.api.domain.greenroom.dto.out.PointAndLevelUpResponseDto; import com.greenroom.server.api.domain.greenroom.service.GreenroomService; @@ -9,7 +8,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.checkerframework.checker.units.qual.A; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -106,6 +104,8 @@ public ResponseEntity getCalender(@AuthenticationPrincipal User use return ResponseEntity.ok(ApiResponse.success(ResponseCodeEnum.SUCCESS,greenroomService.getGreenroomInfoFromCalendar(user.getUsername(),date,activityId))); } - - + @PostMapping("/diary") + public ResponseEntity createDiary(@AuthenticationPrincipal User user, @Valid @RequestPart(value = "data") DiaryCreationRequestDto request, @RequestPart(value = "imageFile",required = false) MultipartFile imageFile){ + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(ResponseCodeEnum.CREATED,greenroomService.createDiary(user.getUsername(),request,imageFile))); + } } diff --git a/server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/in/DiaryCreationRequestDto.java b/server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/in/DiaryCreationRequestDto.java new file mode 100644 index 0000000..544b6e8 --- /dev/null +++ b/server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/in/DiaryCreationRequestDto.java @@ -0,0 +1,19 @@ +package com.greenroom.server.api.domain.greenroom.dto.in; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class DiaryCreationRequestDto { + @NotBlank + @Size(min = 1,max = 85,message = "제목은 최소 1자 이상, 65자 이하로 입력해주세요.") + private final String title; + + @NotBlank + @Size(min = 1,max = 85,message = "본문은 최소 1자 이상, 500자 이하로 입력해주세요.") + private final String content; + + @NotBlank + private final String date; +} diff --git a/server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/out/DiaryResponseDto.java b/server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/out/DiaryResponseDto.java new file mode 100644 index 0000000..13dc6ba --- /dev/null +++ b/server/src/main/java/com/greenroom/server/api/domain/greenroom/dto/out/DiaryResponseDto.java @@ -0,0 +1,25 @@ +package com.greenroom.server.api.domain.greenroom.dto.out; + +import com.greenroom.server.api.domain.greenroom.entity.Diary; +import com.greenroom.server.api.global.config.PropertiesHolder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class DiaryResponseDto { + private final Long greenroomId; + private final String greenroomName; + private final Long diaryId; + private final String title; + private final String content; + private final String imageUrl; + private final String date; + + public static DiaryResponseDto from(Diary diary){ + String imageUrl =null; + if(diary.getDiaryPictureUrl()!=null){imageUrl = PropertiesHolder.CDN_PATH+"/"+diary.getDiaryPictureUrl(); + } + return new DiaryResponseDto(diary.getGreenRoom().getGreenroomId(),diary.getGreenRoom().getName(), diary.getDiaryId(), diary.getTitle(), diary.getContent(), imageUrl, diary.getDate().toString()); + } +} diff --git a/server/src/main/java/com/greenroom/server/api/domain/greenroom/entity/Diary.java b/server/src/main/java/com/greenroom/server/api/domain/greenroom/entity/Diary.java index dcd2c0a..a3ec8a3 100644 --- a/server/src/main/java/com/greenroom/server/api/domain/greenroom/entity/Diary.java +++ b/server/src/main/java/com/greenroom/server/api/domain/greenroom/entity/Diary.java @@ -7,6 +7,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; +import java.time.LocalDateTime; + @Table(name = "diary") @Entity @Getter @@ -16,21 +19,31 @@ public class Diary extends BaseTime { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long diaryId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "greenroom_id") + private GreenRoom greenRoom; + private String diaryPictureUrl; private String title; private String content; + private LocalDate date; + @Builder - public Diary(String diaryPictureUrl, String title, String content, GreenRoom greenRoom) { + public Diary(String diaryPictureUrl, String title, String content, GreenRoom greenRoom, LocalDate date) { this.diaryPictureUrl = diaryPictureUrl; this.title = title; this.content = content; this.greenRoom = greenRoom; + this.date = date; } - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "greenroom_id") - private GreenRoom greenRoom; + public static Diary createDiary(String title, String content, GreenRoom greenRoom, String diaryPictureUrl,LocalDate date){ + return Diary.builder().title(title).content(content).greenRoom(greenRoom).diaryPictureUrl(diaryPictureUrl).date(date).build(); + } + + + } diff --git a/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/DiaryService.java b/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/DiaryService.java index 030f6d8..412f083 100644 --- a/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/DiaryService.java +++ b/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/DiaryService.java @@ -1,15 +1,18 @@ package com.greenroom.server.api.domain.greenroom.service; import com.amazonaws.util.StringUtils; +import com.greenroom.server.api.domain.greenroom.dto.in.DiaryCreationRequestDto; import com.greenroom.server.api.domain.greenroom.entity.Diary; import com.greenroom.server.api.domain.greenroom.entity.GreenRoom; import com.greenroom.server.api.domain.greenroom.repository.DiaryRepository; +import com.greenroom.server.api.global.exception.CustomException; +import com.greenroom.server.api.global.response.enums.ResponseCodeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; @@ -36,4 +39,18 @@ public List deleteAllByGreenRoom(List greenroomIdList){ public List getAllDiariesByGreenroomAndDate(ListgreenRoomList, LocalDate date){ return diaryRepository.findByCreateDateAndGreenRoomIn(date,greenRoomList.stream().map(GreenRoom::getGreenroomId).toList()); } + + public Diary createDiary(GreenRoom greenRoom, DiaryCreationRequestDto request, String imageUrl){ + LocalDate date; + try{ + date = LocalDate.parse(request.getDate()); + } + catch (DateTimeParseException e){ + throw new CustomException(ResponseCodeEnum.INVALID_REQUEST_ARGUMENT); + } + + Diary diary = Diary.createDiary(request.getTitle(),request.getContent(),greenRoom,imageUrl,date); + diaryRepository.save(diary); + return diary; + } } diff --git a/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/GreenroomService.java b/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/GreenroomService.java index c10cfd2..7c3cae4 100644 --- a/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/GreenroomService.java +++ b/server/src/main/java/com/greenroom/server/api/domain/greenroom/service/GreenroomService.java @@ -318,6 +318,7 @@ public GreenroomTodoCycleResponseDto updateActivity(ActivityInfoUpdateRequestDto return getGreenroomTodoInfo(greenroomId); } + public void updateTodo(Todo todo, ActivityInfoUpdateRequestDto.ActivityInfoUpdateDto updateDto){ LocalDate baseDate ; @@ -433,5 +434,20 @@ public List getGreenroomInfoFuture(Us .map(greenroom -> buildCalendarInfo(greenroom, null, todoList.getOrDefault(greenroom, null), null)) .toList(); } + + @Transactional + public DiaryResponseDto createDiary(String email, DiaryCreationRequestDto request, MultipartFile imageFile){ + User user = customUserDetailService.findUserByEmail(email); + List greenRoomList = greenRoomRepository.findGreenRoomByUserAndGreenroomStatus(user,GreenRoomStatus.ENABLED); + if(greenRoomList.isEmpty()){throw new CustomException(ResponseCodeEnum.GREENROOM_NOT_FOUND);} //일기 작성이 가능한 그린룸이 없는 경우 + + String imageUrl = null; + if(imageFile!=null) imageUrl = s3ImageUploader.uploadGreenroomImage(imageFile); + + Diary createdDiary = diaryService.createDiary(greenRoomList.get(0),request,imageUrl); + + return DiaryResponseDto.from(createdDiary); + + } } diff --git a/server/src/main/java/com/greenroom/server/api/utils/S3ImageUploader.java b/server/src/main/java/com/greenroom/server/api/utils/S3ImageUploader.java index 5e2e80d..d0fc760 100644 --- a/server/src/main/java/com/greenroom/server/api/utils/S3ImageUploader.java +++ b/server/src/main/java/com/greenroom/server/api/utils/S3ImageUploader.java @@ -35,6 +35,9 @@ public class S3ImageUploader { @Value("${cloud.image.path.greenroom}") private String greenroomImageDir; + @Value("${cloud.image.path.greenroom}") + private String diaryImageDir; + @Value("${cloud.image.path.plant}") private String plantImageDir; @@ -52,6 +55,10 @@ public String uploadGreenroomImage(MultipartFile multipartFile){ return uploadImage(multipartFile,greenroomImageDir); } + public String uploadDiaryImage(MultipartFile multipartFile){ + return uploadImage(multipartFile,diaryImageDir); + } + public String uploadImage(MultipartFile multipartFile, String dir) { String contentType = multipartFile.getContentType(); diff --git a/server/src/main/resources/application-dev.yml b/server/src/main/resources/application-dev.yml index e4b3874..83506fd 100644 --- a/server/src/main/resources/application-dev.yml +++ b/server/src/main/resources/application-dev.yml @@ -63,6 +63,7 @@ cloud: user: dev/user plant : plant greenroom : dev/greenroom + diary : dev/diary temp: filePath: src/main/resources/ logging.level: diff --git a/server/src/main/resources/application-local.yml b/server/src/main/resources/application-local.yml index b66da38..5be2ee5 100644 --- a/server/src/main/resources/application-local.yml +++ b/server/src/main/resources/application-local.yml @@ -62,6 +62,7 @@ cloud: user: local/user plant : plant greenroom: local/greenroom + diary : local/diary temp: filePath: src/main/resources/ logging.level: diff --git a/server/src/test/java/com/greenroom/server/api/GreenroomIntegrationTest.java b/server/src/test/java/com/greenroom/server/api/GreenroomIntegrationTest.java index bf90a6c..6ad959e 100644 --- a/server/src/test/java/com/greenroom/server/api/GreenroomIntegrationTest.java +++ b/server/src/test/java/com/greenroom/server/api/GreenroomIntegrationTest.java @@ -343,6 +343,18 @@ private MockMultipartFile getInvalidTestMultiPartFile (){ fieldWithPath("isAlive").type(JsonFieldType.BOOLEAN).description("식물 상태. 여전히 키우고 있으면 true, 죽었으면 false") ); + List requestPartDescriptorsForDiaryCreation = List.of( + partWithName("imageFile").description("일기 이미지 파일").attributes(new Attributes.Attribute("content-type","image/*")).optional(), + partWithName("data").description("일기 작성 정보").attributes(new Attributes.Attribute("content-type","application/json")) + ); + + List requestPartFieldDescriptorsForDiaryCreation = List.of( + fieldWithPath("title").type(JsonFieldType.STRING).description("일기 제목").attributes(new Attributes.Attribute("constraint","1자 이상 65자 이하")), + fieldWithPath("content").type(JsonFieldType.STRING).description("일기 본문").attributes(new Attributes.Attribute("constraint","1자 이상 500자 이하")), + fieldWithPath("date").type(JsonFieldType.STRING).description("물주기 기준 날짜").attributes(new Attributes.Attribute("constraint","YYYY-MM-DD")) + ); + + List requestBodyDescriptorsForCompleteTodo = List.of( fieldWithPath("completedTodo").type(JsonFieldType.ARRAY).description("완료 처리할 작업의 id 목록") ); @@ -515,6 +527,20 @@ private MockMultipartFile getInvalidTestMultiPartFile (){ fieldWithPath("data.mainInfo[].diary[].imageUrl").type(JsonFieldType.STRING).description("일기에 등록한 이미지 url").optional().attributes(new Attributes.Attribute("constraint","등록한 이미지가 없을 경우 null"))); + List resultDescriptorsForDiaryCreation = List.of( + fieldWithPath("status").type(JsonFieldType.STRING).description("응답 상태"), + fieldWithPath("code").type(JsonFieldType.STRING).description("상태 코드"), + fieldWithPath("data").type(JsonFieldType.OBJECT).optional().description("data"), + fieldWithPath("data.diaryId").type(JsonFieldType.NUMBER).description("새롭게 생성된 다이어리 고유 id"), + fieldWithPath("data.greenroomId").type(JsonFieldType.NUMBER).description("그린룸 id"), + fieldWithPath("data.greenroomName").type(JsonFieldType.STRING).description("그린룸 별명").optional(), + fieldWithPath("data.title").type(JsonFieldType.STRING).description("일기 제목"), + fieldWithPath("data.content").type(JsonFieldType.STRING).description("일기 본문"), + fieldWithPath("data.imageUrl").type(JsonFieldType.STRING).description("일기 이미지").optional().attributes(new Attributes.Attribute("constraint","등록된 이미지가 없으면 null")), + fieldWithPath("data.date").type(JsonFieldType.STRING).description("일기 작성 날짜") + ); + + @Transactional @Test public void 그린룸_정보_조회_성공1() throws Exception { @@ -1551,5 +1577,126 @@ private RestDocumentationResultHandler getDocumentForCalenderInfo(Integer identi resultActions.andDo(getDocumentForCalenderInfo(1)); } + private ResultActions getResultActionsForDiaryCreation(MockMultipartFile image,MockMultipartFile data) throws Exception { + + String token = getTokenForTest((long) (10*1000)); + + return mockMvc.perform( // api 실행 + RestDocumentationRequestBuilders + .multipart("/api/greenroom/diary") + .file(image) + .file(data) + .contentType(MediaType.MULTIPART_FORM_DATA) + .header(HttpHeaders.AUTHORIZATION, "Bearer "+token)); + } + + private RestDocumentationResultHandler getDocumentForDiaryCreation(Integer identifier){ + return document("api/greenroom/diary/post/"+identifier, + preprocessRequest(prettyPrint(),modifyUris().scheme("https").host("greenroom-server.site").removePort()), // (2) + preprocessResponse(prettyPrint(), getModifiedHeader()), // (3) + requestParts(requestPartDescriptorsForDiaryCreation), + requestPartFields("data",requestPartFieldDescriptorsForDiaryCreation), + responseFields(resultDescriptorsForDiaryCreation), // responseBody 설명 + requestHeaders(headerWithName("Authorization").description("Bearer : 사용자 access Token")), + resource( + ResourceSnippetParameters.builder() + .tag("그린룸") // 문서에서 api들이 태그로 분류됨 + .summary("일기 작성 api") // api 이름 + .description("그린룸 일기 작성") // api 설명 + .build())); + } + + @Test + @Transactional + public void 일기_작성_성공() throws Exception { + //given + + //test용 data 생성 + + User user =signupForTest(); + createGreenRoom(user); + + DiaryCreationRequestDto request = new DiaryCreationRequestDto("일기 제목입니다!!","일기 본문입니다!!","2025-04-05"); + MockMultipartFile image = getTestMultiPartFile(); + MockMultipartFile data = new MockMultipartFile("data", "", "application/json", mapper.writeValueAsString(request).getBytes()); + + //when + ResultActions resultActions = getResultActionsForDiaryCreation(image,data); + + //then + resultActions.andExpect(status().isCreated()); + + //문서화 + resultActions.andDo(getDocumentForDiaryCreation(1)); + + } + + @Test + @Transactional + public void 일기_작성_실패1() throws Exception { + //given + + //test용 data 생성 + User user =signupForTest(); + + DiaryCreationRequestDto request = new DiaryCreationRequestDto("일기 제목입니다!!","일기 본문입니다!!","2025-04-05"); + MockMultipartFile image = getTestMultiPartFile(); + MockMultipartFile data = new MockMultipartFile("data", "", "application/json", mapper.writeValueAsString(request).getBytes()); + + //when + ResultActions resultActions = getResultActionsForDiaryCreation(image,data); + + //then + resultActions.andExpect(status().is(ResponseCodeEnum.GREENROOM_NOT_FOUND.getStatus().value())).andExpect(jsonPath("code").value(ResponseCodeEnum.GREENROOM_NOT_FOUND.getCode())); + + //문서화 + resultActions.andDo(getDocumentForDiaryCreation(2)); + } + + @Test + @Transactional + public void 일기_작성_실패2() throws Exception { + //given + + //test 용 data + User user = signupForTest(); + createGreenRoom(user); + + DiaryCreationRequestDto request = new DiaryCreationRequestDto("일기 제목입니다!!","일기 본문입니다!!","2025-04-05"); + MockMultipartFile image = getInvalidTestMultiPartFile(); + MockMultipartFile data = new MockMultipartFile("data", "", "application/json", mapper.writeValueAsString(request).getBytes()); + + //when + ResultActions resultActions = getResultActionsForDiaryCreation(image,data); + + //then + resultActions.andExpect(status().is(ResponseCodeEnum.INVALID_IMAGE_FORMAT.getStatus().value())).andExpect(jsonPath("code").value(ResponseCodeEnum.INVALID_IMAGE_FORMAT.getCode())); + + //문서화 + resultActions.andDo(getDocumentForDiaryCreation(3)); + } + + @Test + @Transactional + public void 일기_작성_실패3() throws Exception { + //given + //test 용 data + User user =signupForTest(); + + DiaryCreationRequestDto request = new DiaryCreationRequestDto("일기 제목임~!!","일기 본문입니다!!","2025-04-05"); + MockMultipartFile image = getTestMultiPartFile(); + MockMultipartFile data = new MockMultipartFile("data", "", "application/json", mapper.writeValueAsString(request).getBytes()); + + //when + doThrow(new CustomException(ResponseCodeEnum.FAIL_TO_UPLOAD_IMAGE)).when(mockitoGreenroomService).createDiary(EMAIL,request,image); + ResultActions resultActions = getResultActionsForDiaryCreation(image,data); + + //then + resultActions.andExpect(status().is(ResponseCodeEnum.FAIL_TO_UPLOAD_IMAGE.getStatus().value())).andExpect(jsonPath("code").value(ResponseCodeEnum.FAIL_TO_UPLOAD_IMAGE.getCode())); + + //문서화 + resultActions.andDo(getDocumentForDiaryCreation(4)); + } + } diff --git a/server/src/test/resources/schema-test.sql b/server/src/test/resources/schema-test.sql index 9874f29..495fd20 100644 --- a/server/src/test/resources/schema-test.sql +++ b/server/src/test/resources/schema-test.sql @@ -128,6 +128,7 @@ CREATE TABLE `diary` ( `diary_picture_url` varchar(255), `title` varchar(255), `content` varchar(1000), + `date` date, `create_date` timestamp DEFAULT CURRENT_TIMESTAMP, `update_date` timestamp ON UPDATE CURRENT_TIMESTAMP );