From a055766a0547785e3dfd973866af3b9ee8954309 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:23:20 +0900 Subject: [PATCH 01/24] =?UTF-8?q?Chore:=20presentation=EB=8B=A8=EC=9D=98?= =?UTF-8?q?=20String=ED=83=80=EC=9E=85=20Json=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/knu/ddip/common/dto/StringTypeResponse.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/com/knu/ddip/common/dto/StringTypeResponse.java diff --git a/src/main/java/com/knu/ddip/common/dto/StringTypeResponse.java b/src/main/java/com/knu/ddip/common/dto/StringTypeResponse.java new file mode 100644 index 0000000..1e5dcbc --- /dev/null +++ b/src/main/java/com/knu/ddip/common/dto/StringTypeResponse.java @@ -0,0 +1,6 @@ +package com.knu.ddip.common.dto; + +public record StringTypeResponse( + String response +) { +} From 904e4fffd25176b40ddae51730153ffa2393d4c5 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:23:43 +0900 Subject: [PATCH 02/24] =?UTF-8?q?Feat:=20User=EA=B4=80=EB=A0=A8=20exceptio?= =?UTF-8?q?n=20global=20handler=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java b/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java index e0ce4bc..e5007ee 100644 --- a/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java @@ -1,10 +1,13 @@ package com.knu.ddip.common.exception; import com.knu.ddip.auth.exception.*; +import com.knu.ddip.common.file.FileStorageException; import com.knu.ddip.ddipevent.exception.DdipBadRequestException; import com.knu.ddip.ddipevent.exception.DdipForbiddenException; import com.knu.ddip.ddipevent.exception.DdipNotFoundException; import com.knu.ddip.location.exception.LocationNotFoundException; +import com.knu.ddip.user.exception.UserEmailDuplicateException; +import com.knu.ddip.user.exception.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; @@ -103,4 +106,27 @@ public ResponseEntity handleDdipForbiddenException(DdipForbiddenE return ResponseEntity.status(HttpStatus.FORBIDDEN).body(problemDetail); } + @ExceptionHandler(UserEmailDuplicateException.class) + public ResponseEntity handleUserEmailDuplicateException(UserEmailDuplicateException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); + problemDetail.setTitle("User Email Duplicate"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); + } + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity handleUserNotFoundException(UserNotFoundException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); + problemDetail.setTitle("User NotFound"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); + } + + @ExceptionHandler(FileStorageException.class) + public ResponseEntity handleFileStorageException(FileStorageException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + problemDetail.setTitle("File Storage Exception"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail); + } } From 4248c237dd2a1c839924fcd9ab79dab483408292 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:24:19 +0900 Subject: [PATCH 03/24] =?UTF-8?q?Feat:=20DDIP=20Event=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=95=A0=EA=B7=B8=EB=A6=AC=EA=B1=B0=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knu/ddip/ddipevent/domain/DdipEvent.java | 136 +++++++++++++++++- 1 file changed, 133 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java b/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java index 8f5cd40..cc76521 100644 --- a/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java +++ b/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java @@ -2,6 +2,7 @@ import com.knu.ddip.ddipevent.exception.DdipBadRequestException; import com.knu.ddip.ddipevent.exception.DdipForbiddenException; +import com.knu.ddip.ddipevent.exception.DdipNotFoundException; import lombok.Builder; import lombok.Getter; @@ -43,7 +44,7 @@ public static DdipEvent create(String title, String content, Integer reward, Dou .build(); } - public void apply(UUID applicantId) { + public DdipEvent apply(UUID applicantId) { if (this.status != DdipStatus.OPEN) { throw new DdipBadRequestException("이미 마감된 띱입니다."); } @@ -56,11 +57,13 @@ public void apply(UUID applicantId) { .actorId(applicantId) .actorRole(ActorRole.RESPONDER) .actionType(ActionType.APPLY) + .timestamp(Instant.now()) .build()); - } + return this; + } else throw new DdipBadRequestException("이미 지원한 띱입니다."); } - public void selectResponder(UUID requesterId, UUID responderId) { + public DdipEvent selectResponder(UUID requesterId, UUID responderId) { if (!Objects.equals(this.requesterId, requesterId)) { throw new DdipForbiddenException("띱을 등록한 사용자만 수행자를 선택할 수 있습니다."); } @@ -76,6 +79,133 @@ public void selectResponder(UUID requesterId, UUID responderId) { .actorId(requesterId) .actorRole(ActorRole.REQUESTER) .actionType(ActionType.SELECT_RESPONDER) + .timestamp(Instant.now()) + .build()); + return this; + } + + public DdipEvent uploadPhoto(UUID responderId, String photoUrl, Double latitude, Double longitude, String responderComment) { + if (!this.selectedResponderId.equals(responderId)) { + throw new DdipForbiddenException("띱의 선택된 수행자만 사진을 업로드할 수 있습니다."); + } + if (this.status != DdipStatus.IN_PROGRESS) { + throw new DdipBadRequestException("진행중인 띱에만 사진을 업로드할 수 있습니다."); + } + if (photoUrl == null || photoUrl.isBlank()) { + throw new DdipBadRequestException("photoUrl 값이 없습니다."); + } + this.photos.add(Photo.builder() + .photoUrl(photoUrl) + .latitude(latitude) + .longitude(longitude) + .timestamp(Instant.now()) + .status(PhotoStatus.PENDING) + .responderComment(responderComment) .build()); + this.interactions.add(Interaction.builder() + .actorId(responderId) + .actorRole(ActorRole.RESPONDER) + .actionType(ActionType.SUBMIT_PHOTO) + .timestamp(Instant.now()) + .build()); + return this; + } + + public DdipEvent updatePhotoFeedback(UUID requesterOrResponderId, UUID photoId, PhotoStatus status, String feedback) { + if (status.equals(PhotoStatus.PENDING)) { + throw new DdipForbiddenException("Pending 상태로 변경할수는 없습니다."); + } + Photo targetPhoto = findPhotoOrThrow(photoId); + if (Objects.equals(requesterOrResponderId, this.requesterId)) { // 주체가 요청자 + if (status.equals(PhotoStatus.APPROVED)) { + targetPhoto.approve(); + } else { + boolean isQuestion = targetPhoto.feedbackByRequester(feedback); + if (isQuestion) { + this.interactions.add(Interaction.builder() + .actorId(this.requesterId) + .actorRole(ActorRole.REQUESTER) + .actionType(ActionType.ASK_QUESTION) + .comment(feedback) + .relatedPhotoId(photoId) + .timestamp(Instant.now()) + .build()); + } else { + this.interactions.add(Interaction.builder() + .actorId(this.requesterId) + .actorRole(ActorRole.REQUESTER) + .actionType(ActionType.REQUEST_REVISION) + .comment(feedback) + .relatedPhotoId(photoId) + .timestamp(Instant.now()) + .build()); + } + } + return this; + } else if (Objects.equals(requesterOrResponderId, this.selectedResponderId)) { // 주체가 수행자 + targetPhoto.feedbackByResponder(feedback); + this.interactions.add(Interaction.builder() + .actorId(this.selectedResponderId) + .actorRole(ActorRole.RESPONDER) + .actionType(ActionType.ANSWER_QUESTION) + .comment(feedback) + .relatedPhotoId(photoId) + .timestamp(Instant.now()) + .build()); + return this; + } else + throw new DdipBadRequestException("띱의 수행자 또는 요청자만 사진에 피드백을 남길 수 있습니다."); + } + + public DdipEvent complete(UUID requesterId) { + if (!Objects.equals(this.requesterId, requesterId)) { + throw new DdipForbiddenException("띱을 등록한 사용자만 완료할 수 있습니다."); + } + if (photos.isEmpty()) { + throw new DdipBadRequestException("업로드된 사진이 없습니다."); + } + Photo lastPhoto = photos.get(photos.size() - 1); + if (lastPhoto.statusIsNotApproved()) { + throw new DdipForbiddenException("최종 사진의 Status가 Approved가 아닙니다."); + } + this.status = DdipStatus.COMPLETED; + this.interactions.add(Interaction.builder() + .actorId(requesterId) + .actorRole(ActorRole.RESPONDER) + .actionType(ActionType.APPROVE) + .timestamp(Instant.now()) + .build()); + return this; + } + + public DdipEvent cancel(UUID requesterOrResponderId) { + if (Objects.equals(requesterOrResponderId, this.requesterId)) { // 주체가 요청자 + this.status = DdipStatus.CANCELED; + this.interactions.add(Interaction.builder() + .actorId(this.requesterId) + .actorRole(ActorRole.REQUESTER) + .actionType(ActionType.CANCEL_BY_REQUESTER) + .timestamp(Instant.now()) + .build()); + return this; + } else if (Objects.equals(requesterOrResponderId, this.selectedResponderId)) { // 주체가 수행자 + this.status = DdipStatus.CANCELED; + this.interactions.add(Interaction.builder() + .actorId(this.selectedResponderId) + .actorRole(ActorRole.RESPONDER) + .actionType(ActionType.GIVE_UP_BY_RESPONDER) + .timestamp(Instant.now()) + .build()); + return this; + } else + throw new DdipBadRequestException("띱의 수행자 또는 요청자만 취소할 수 있습니다."); + } + + private Photo findPhotoOrThrow(UUID photoId) { + return this.photos.stream() + .filter(photo -> photo.getPhotoId().equals(photoId)) + .findFirst() + .orElseThrow(() -> + new DdipNotFoundException("해당 ID를 가진 Photo가 존재하지 않습니다")); } } From 72c26400b50e8b4d8f52d1a3cd7f76a7295c3ddd Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:24:39 +0900 Subject: [PATCH 04/24] =?UTF-8?q?Feat:=20=EB=A3=A8=ED=8A=B8=20=EC=95=A0?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B1=B0=ED=8A=B8=20=ED=95=98=EC=9C=84=20Pho?= =?UTF-8?q?to=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/knu/ddip/ddipevent/domain/Photo.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/com/knu/ddip/ddipevent/domain/Photo.java b/src/main/java/com/knu/ddip/ddipevent/domain/Photo.java index 06fa129..83ce880 100644 --- a/src/main/java/com/knu/ddip/ddipevent/domain/Photo.java +++ b/src/main/java/com/knu/ddip/ddipevent/domain/Photo.java @@ -1,5 +1,7 @@ package com.knu.ddip.ddipevent.domain; +import com.knu.ddip.ddipevent.exception.DdipBadRequestException; +import com.knu.ddip.ddipevent.exception.DdipForbiddenException; import lombok.Builder; import lombok.Getter; @@ -24,7 +26,29 @@ public void approve() { this.status = PhotoStatus.APPROVED; } + public boolean feedbackByRequester(String feedback) { + if (this.requesterQuestion == null) { + this.requesterQuestion = feedback; + return true; + } else if (this.responderAnswer != null) { + reject(); + this.rejectionReason = feedback; + return false; + } else throw new DdipBadRequestException("요청자가 남긴 질문에 대한 수행자의 응답 없이 사진을 반려할 수 없습니다."); + } + public void reject() { this.status = PhotoStatus.REJECTED; } + + public void feedbackByResponder(String feedback) { + if (this.requesterQuestion == null) { + throw new DdipForbiddenException("요청자의 질문 없이 질문에 대한 대답을 남길 수 없습니다."); + } + this.responderAnswer = feedback; + } + + public boolean statusIsNotApproved() { + return !this.status.equals(PhotoStatus.APPROVED); + } } From f460515d9cdae6f973b097f96ca541de768b31f6 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:24:57 +0900 Subject: [PATCH 05/24] =?UTF-8?q?Feat:=20Action=20type=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/knu/ddip/ddipevent/domain/ActionType.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/knu/ddip/ddipevent/domain/ActionType.java b/src/main/java/com/knu/ddip/ddipevent/domain/ActionType.java index 8ac840f..8c623f7 100644 --- a/src/main/java/com/knu/ddip/ddipevent/domain/ActionType.java +++ b/src/main/java/com/knu/ddip/ddipevent/domain/ActionType.java @@ -7,12 +7,14 @@ public enum ActionType { APPROVE, REQUEST_REVISION, CANCEL_BY_REQUESTER, + ASK_QUESTION, // RESPONDER actions APPLY, SUBMIT_PHOTO, REPORT_SITUATION, GIVE_UP_BY_RESPONDER, + ANSWER_QUESTION, // SYSTEM actions EXPIRE, From 6b1398394bee9b7efb9dd23559d5e3fee5e21d1a Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:25:18 +0900 Subject: [PATCH 06/24] =?UTF-8?q?Fix:=20Mapper=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B9=88=20List=20=EC=B4=88=EA=B8=B0=ED=99=94=EC=9D=98=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EA=B0=80=EB=B3=80=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddipevent/infrastructure/DdipMapper.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java b/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java index a440958..a5f495b 100644 --- a/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java +++ b/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java @@ -10,7 +10,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -38,13 +40,13 @@ private DdipEventEntity buildDdipEventEntity(DdipEvent domain) { .createdAt(domain.getCreatedAt()) .status(domain.getStatus()) .selectedResponderId(domain.getSelectedResponderId()) - .applicants(domain.getApplicants()) + .applicants(domain.getApplicants() != null ? domain.getApplicants() : new ArrayList<>()) .difficulty(domain.getDifficulty()) .build(); } private List mapPhotos(List photos, DdipEventEntity ddipEvent) { - if (photos == null) return List.of(); + if (photos == null) return new ArrayList<>(); return photos.stream() .map(photo -> PhotoEntity.builder() .id(photo.getPhotoId()) @@ -59,11 +61,11 @@ private List mapPhotos(List photos, DdipEventEntity ddipEven .responderAnswer(photo.getResponderAnswer()) .rejectionReason(photo.getRejectionReason()) .build()) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); } private List mapInteractions(List interactions, DdipEventEntity ddipEvent) { - if (interactions == null) return List.of(); + if (interactions == null) return new ArrayList<>(); return interactions.stream() .map(interaction -> InteractionEntity.builder() .id(interaction.getInteractionId()) @@ -75,7 +77,7 @@ private List mapInteractions(List interactions, .relatedPhotoId(interaction.getRelatedPhotoId()) .timestamp(interaction.getTimestamp()) .build()) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); } public DdipEvent toDomain(DdipEventEntity entity) { @@ -99,7 +101,7 @@ public DdipEvent toDomain(DdipEventEntity entity) { } private List mapPhotoDomain(List photoEntities) { - if (photoEntities == null) return List.of(); + if (photoEntities == null) return new ArrayList<>(); return photoEntities.stream() .map(pe -> Photo.builder() .photoId(pe.getId()) @@ -113,11 +115,11 @@ private List mapPhotoDomain(List photoEntities) { .responderAnswer(pe.getResponderAnswer()) .rejectionReason(pe.getRejectionReason()) .build()) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); } private List mapInteractionDomain(List interactionEntities) { - if (interactionEntities == null) return List.of(); + if (interactionEntities == null) return new ArrayList<>(); return interactionEntities.stream() .map(ie -> Interaction.builder() .interactionId(ie.getId()) @@ -128,6 +130,6 @@ private List mapInteractionDomain(List interacti .relatedPhotoId(ie.getRelatedPhotoId()) .timestamp(ie.getTimestamp()) .build()) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); } } From 0e507cc8c0574c5d5fde302190313d57c8df2460 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:25:50 +0900 Subject: [PATCH 07/24] =?UTF-8?q?Feat:=20DDIP=20Service=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/DdipService.java | 69 +++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java b/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java index 45cc6d8..10fb79a 100644 --- a/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java +++ b/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java @@ -1,5 +1,6 @@ package com.knu.ddip.ddipevent.application.service; +import com.knu.ddip.common.file.FileStorageService; import com.knu.ddip.ddipevent.application.dto.*; import com.knu.ddip.ddipevent.domain.DdipEvent; import com.knu.ddip.ddipevent.exception.DdipNotFoundException; @@ -19,6 +20,7 @@ public class DdipService { private final DdipEventRepository ddipEventRepository; private final UserRepository userRepository; + private final FileStorageService fileStorageService; @Transactional public DdipEventDetailDto createDdipEvent(CreateDdipRequestDto dto, UUID requesterId) { @@ -39,12 +41,68 @@ public List getDdipEventFeed(FeedRequestDto dto) { .toList(); } - public DdipEventDetailDto getDdipEventDetail(UUID ddipId) { - DdipEvent event = ddipEventRepository.findById(ddipId) - .orElseThrow(() -> new DdipNotFoundException("Ddip event를 찾을 수 없습니다.")); + public DdipEventDetailDto getDdipEventDetail(UUID eventId) { + DdipEvent event = getDdipEvent(eventId); return convertToDetailDto(event); } + @Transactional + public void applyDdipEvent(UUID eventId, UUID responderId) { + UserEntityDto responder = userRepository.getById(responderId); + DdipEvent event = getDdipEvent(eventId); + DdipEvent updatedEvent = event.apply(responder.getId()); + ddipEventRepository.save(updatedEvent); + } + + @Transactional + public void selectApplicantForDdipEvent(UUID eventId, SelectApplicantRequest selectApplicantRequest, UUID requesterId) { + UserEntityDto requester = userRepository.getById(requesterId); + UserEntityDto responder = userRepository.getById(selectApplicantRequest.applicantId()); + DdipEvent event = getDdipEvent(eventId); + DdipEvent updatedEvent = event.selectResponder(requester.getId(), responder.getId()); + ddipEventRepository.save(updatedEvent); + } + + @Transactional + public DdipEventDetailDto uploadPhotoForDdipEvent(UUID eventId, PhotoUploadRequest photoUploadRequest, UUID responderId) { + UserEntityDto responder = userRepository.getById(responderId); + DdipEvent event = getDdipEvent(eventId); + + String photoUrl = fileStorageService.uploadFile(photoUploadRequest.photo(), "photos"); + + DdipEvent updatedEvent = event.uploadPhoto(responder.getId(), photoUrl, photoUploadRequest.latitude(), photoUploadRequest.longitude(), photoUploadRequest.responderComment()); + return convertToDetailDto(ddipEventRepository.save(updatedEvent)); + } + + @Transactional + public DdipEventDetailDto updatePhotoFeedback(UUID eventId, UUID photoId, PhotoFeedbackRequest photoFeedbackRequest, UUID requesterOrResponderId) { + UserEntityDto requesterOrResponder = userRepository.getById(requesterOrResponderId); + DdipEvent event = getDdipEvent(eventId); + DdipEvent updatedEvent = event.updatePhotoFeedback(requesterOrResponder.getId(), photoId, photoFeedbackRequest.status(), photoFeedbackRequest.feedback()); + return convertToDetailDto(ddipEventRepository.save(updatedEvent)); + } + + @Transactional + public DdipEventDetailDto completeDdipEventMission(UUID eventId, UUID requesterId) { + UserEntityDto requester = userRepository.getById(requesterId); + DdipEvent event = getDdipEvent(eventId); + DdipEvent updatedEvent = event.complete(requester.getId()); + return convertToDetailDto(ddipEventRepository.save(updatedEvent)); + } + + @Transactional + public DdipEventDetailDto cancelDdipEventMission(UUID eventId, UUID requesterOrResponderId) { + UserEntityDto requesterOrResponder = userRepository.getById(requesterOrResponderId); + DdipEvent event = getDdipEvent(eventId); + DdipEvent updatedEvent = event.cancel(requesterOrResponder.getId()); + return convertToDetailDto(ddipEventRepository.save(updatedEvent)); + } + + private DdipEvent getDdipEvent(UUID eventId) { + return ddipEventRepository.findById(eventId) + .orElseThrow(() -> new DdipNotFoundException("Ddip event를 찾을 수 없습니다.")); + } + private DdipEventSummaryDto convertToSummaryDto(DdipEvent event) { // TODO: distance(요청자와 사용자 사이의 거리) 계산 로직 추가 return new DdipEventSummaryDto( @@ -74,8 +132,9 @@ private DdipEventDetailDto convertToDetailDto(DdipEvent event) { event.getLongitude(), event.getStatus(), event.getCreatedAt().toString(), - null, // TODO: applicants - null, // TODO: selectedResponder + event.getApplicants().stream().map(UUID::toString).map(UserSummaryDto::fromUserId).toList(), // TODO: 세부 구현 + event.getSelectedResponderId() != null + ? UserSummaryDto.fromUserId(event.getSelectedResponderId().toString()) : null, // TODO: 세부 구현 event.getPhotos().stream() .map(PhotoDto::fromEntity) .toList(), From 0179b7c5e4d9689581f34e2ed325e46c2062ba9c Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:26:03 +0900 Subject: [PATCH 08/24] =?UTF-8?q?Chore:=20S3=20=EA=B5=AC=ED=98=84=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/knu/ddip/common/config/S3Config.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/com/knu/ddip/common/config/S3Config.java diff --git a/src/main/java/com/knu/ddip/common/config/S3Config.java b/src/main/java/com/knu/ddip/common/config/S3Config.java new file mode 100644 index 0000000..9d6d26c --- /dev/null +++ b/src/main/java/com/knu/ddip/common/config/S3Config.java @@ -0,0 +1,32 @@ +package com.knu.ddip.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public S3Client s3Client() { + AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey); + + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) + .build(); + } +} From 9a89a53773d8fb42e7a11607ea8bae8a9f88d7ac Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:26:14 +0900 Subject: [PATCH 09/24] =?UTF-8?q?Chore:=20S3=20=EA=B5=AC=ED=98=84=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20gradle=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index d60d66f..2e9b0b6 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,11 @@ dependencies { implementation 'org.springframework.session:spring-session-data-redis' implementation 'org.redisson:redisson-spring-boot-starter:3.23.1' + // AWS S3 + implementation 'software.amazon.awssdk:s3:2.21.29' + implementation 'software.amazon.awssdk:auth:2.21.29' + implementation 'software.amazon.awssdk:regions:2.21.29' + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' From ba66648c350603d7ceec49d7b6d9f61aa2c641cc Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:26:36 +0900 Subject: [PATCH 10/24] =?UTF-8?q?Feat:=20File=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=EC=84=9C=EC=9D=98=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=EB=90=9C=20exception=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/knu/ddip/common/file/FileStorageException.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/knu/ddip/common/file/FileStorageException.java diff --git a/src/main/java/com/knu/ddip/common/file/FileStorageException.java b/src/main/java/com/knu/ddip/common/file/FileStorageException.java new file mode 100644 index 0000000..035288d --- /dev/null +++ b/src/main/java/com/knu/ddip/common/file/FileStorageException.java @@ -0,0 +1,8 @@ +package com.knu.ddip.common.file; + +public class FileStorageException extends RuntimeException { + + public FileStorageException(String message) { + super(message); + } +} From 8bf14296467fd9d834809e9bb499e4fe5354982f Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:26:50 +0900 Subject: [PATCH 11/24] =?UTF-8?q?Feat:=20File=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/knu/ddip/common/file/FileStorageService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/knu/ddip/common/file/FileStorageService.java diff --git a/src/main/java/com/knu/ddip/common/file/FileStorageService.java b/src/main/java/com/knu/ddip/common/file/FileStorageService.java new file mode 100644 index 0000000..2240d44 --- /dev/null +++ b/src/main/java/com/knu/ddip/common/file/FileStorageService.java @@ -0,0 +1,12 @@ +package com.knu.ddip.common.file; + +import org.springframework.web.multipart.MultipartFile; + +public interface FileStorageService { + + String uploadFile(MultipartFile file, String directory); + + void deleteFile(String fileUrl); + + boolean exists(String fileUrl); +} From 8dc46239eec9de39ad28bdf622f7228ee02ac882 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:27:03 +0900 Subject: [PATCH 12/24] =?UTF-8?q?Feat:=20=ED=8C=8C=EC=9D=BC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20S3=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/S3FileStorageService.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/main/java/com/knu/ddip/common/file/infrastructure/S3FileStorageService.java diff --git a/src/main/java/com/knu/ddip/common/file/infrastructure/S3FileStorageService.java b/src/main/java/com/knu/ddip/common/file/infrastructure/S3FileStorageService.java new file mode 100644 index 0000000..db592c2 --- /dev/null +++ b/src/main/java/com/knu/ddip/common/file/infrastructure/S3FileStorageService.java @@ -0,0 +1,142 @@ +package com.knu.ddip.common.file.infrastructure; + +import com.knu.ddip.common.file.FileStorageException; +import com.knu.ddip.common.file.FileStorageService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class S3FileStorageService implements FileStorageService { + + private final S3Client s3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + @Override + public String uploadFile(MultipartFile file, String directory) { + validateFile(file); + + try { + String fileName = generateUniqueFileName(file.getOriginalFilename()); + String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String key = String.format("%s/%s/%s", directory, dateDir, fileName); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(file.getContentType()) + .contentLength(file.getSize()) + .build(); + + s3Client.putObject(putObjectRequest, + RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + + String fileUrl = String.format("https://%s.s3.amazonaws.com/%s", bucketName, key); + + log.info("S3 파일 업로드 완료: {} -> {}", file.getOriginalFilename(), fileUrl); + return fileUrl; + + } catch (IOException e) { + log.error("S3 파일 업로드 실패: {}", file.getOriginalFilename(), e); + throw new FileStorageException("파일 업로드에 실패했습니다: " + e.getMessage()); + } catch (S3Exception e) { + log.error("S3 서비스 오류: {}", e.awsErrorDetails().errorMessage()); + throw new FileStorageException("S3 업로드 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + @Override + public void deleteFile(String fileUrl) { + try { + String key = extractS3Key(fileUrl); + + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + s3Client.deleteObject(deleteObjectRequest); + log.info("S3 파일 삭제 완료: {}", fileUrl); + + } catch (S3Exception e) { + log.error("S3 파일 삭제 실패: {}", fileUrl, e); + throw new FileStorageException("파일 삭제에 실패했습니다: " + e.getMessage()); + } + } + + @Override + public boolean exists(String fileUrl) { + try { + String key = extractS3Key(fileUrl); + + HeadObjectRequest headObjectRequest = HeadObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + s3Client.headObject(headObjectRequest); + return true; + + } catch (NoSuchKeyException e) { + return false; + } catch (S3Exception e) { + log.error("S3 파일 존재 여부 확인 실패: {}", fileUrl, e); + return false; + } + } + + private String generateUniqueFileName(String originalFilename) { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HHmmss")); + String uuid = UUID.randomUUID().toString().substring(0, 8); + String extension = getFileExtension(originalFilename); + return String.format("%s_%s%s", timestamp, uuid, extension); + } + + private String getFileExtension(String filename) { + if (filename == null || filename.isEmpty()) { + return ""; + } + int lastDotIndex = filename.lastIndexOf('.'); + return (lastDotIndex == -1) ? "" : filename.substring(lastDotIndex); + } + + private String extractS3Key(String fileUrl) { + if (fileUrl.contains(".s3.amazonaws.com/")) { + return fileUrl.substring(fileUrl.indexOf(".s3.amazonaws.com/") + 18); + } + + throw new IllegalArgumentException("올바르지 않은 S3 URL 형식입니다: " + fileUrl); + } + + private void validateFile(MultipartFile file) { + if (file.isEmpty()) { + throw new FileStorageException("빈 파일은 업로드할 수 없습니다."); + } + + // 파일 크기 제한 (50MB) + long maxSize = 50 * 1024 * 1024; + if (file.getSize() > maxSize) { + throw new FileStorageException("파일 크기는 50MB를 초과할 수 없습니다."); + } + + // 이미지 파일만 허용 + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new FileStorageException("이미지 파일만 업로드 가능합니다."); + } + } +} From 98ef7b480686bdc3b0fd7ed7f6ba225476305528 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:27:21 +0900 Subject: [PATCH 13/24] =?UTF-8?q?Feat:=20DTO=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/PhotoFeedbackRequest.java | 9 +++++++++ .../ddipevent/application/dto/PhotoUploadRequest.java | 11 +++++++++++ .../application/dto/SelectApplicantRequest.java | 8 ++++++++ .../ddipevent/application/dto/UserSummaryDto.java | 4 ++++ 4 files changed, 32 insertions(+) create mode 100644 src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java create mode 100644 src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoUploadRequest.java create mode 100644 src/main/java/com/knu/ddip/ddipevent/application/dto/SelectApplicantRequest.java diff --git a/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java b/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java new file mode 100644 index 0000000..662dcd8 --- /dev/null +++ b/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java @@ -0,0 +1,9 @@ +package com.knu.ddip.ddipevent.application.dto; + +import com.knu.ddip.ddipevent.domain.PhotoStatus; + +public record PhotoFeedbackRequest( + PhotoStatus status, + String feedback +) { +} diff --git a/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoUploadRequest.java b/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoUploadRequest.java new file mode 100644 index 0000000..c8818fb --- /dev/null +++ b/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoUploadRequest.java @@ -0,0 +1,11 @@ +package com.knu.ddip.ddipevent.application.dto; + +import org.springframework.web.multipart.MultipartFile; + +public record PhotoUploadRequest( + MultipartFile photo, + double latitude, + double longitude, + String responderComment +) { +} diff --git a/src/main/java/com/knu/ddip/ddipevent/application/dto/SelectApplicantRequest.java b/src/main/java/com/knu/ddip/ddipevent/application/dto/SelectApplicantRequest.java new file mode 100644 index 0000000..b79cd09 --- /dev/null +++ b/src/main/java/com/knu/ddip/ddipevent/application/dto/SelectApplicantRequest.java @@ -0,0 +1,8 @@ +package com.knu.ddip.ddipevent.application.dto; + +import java.util.UUID; + +public record SelectApplicantRequest( + UUID applicantId +) { +} diff --git a/src/main/java/com/knu/ddip/ddipevent/application/dto/UserSummaryDto.java b/src/main/java/com/knu/ddip/ddipevent/application/dto/UserSummaryDto.java index 036e440..2c5a433 100644 --- a/src/main/java/com/knu/ddip/ddipevent/application/dto/UserSummaryDto.java +++ b/src/main/java/com/knu/ddip/ddipevent/application/dto/UserSummaryDto.java @@ -10,4 +10,8 @@ public record UserSummaryDto( Integer responderMissionCount, BadgeDto representativeBadge ) { + public static UserSummaryDto fromUserId(String userId) { // TODO : 구현 + return new UserSummaryDto(userId, null, null, null, null, + null, null, null); + } } From aff67e206c7ef25b30d3afa15e45a4d7a7204f18 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:27:40 +0900 Subject: [PATCH 14/24] =?UTF-8?q?Feat:=20DDIP=20API=20=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=EB=8B=A8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddipevent/presentation/api/DdipApi.java | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/knu/ddip/ddipevent/presentation/api/DdipApi.java b/src/main/java/com/knu/ddip/ddipevent/presentation/api/DdipApi.java index e0f8478..f9ecb3d 100644 --- a/src/main/java/com/knu/ddip/ddipevent/presentation/api/DdipApi.java +++ b/src/main/java/com/knu/ddip/ddipevent/presentation/api/DdipApi.java @@ -3,13 +3,12 @@ import com.knu.ddip.auth.domain.AuthUser; import com.knu.ddip.auth.presentation.annotation.Login; import com.knu.ddip.auth.presentation.annotation.RequireAuth; -import com.knu.ddip.ddipevent.application.dto.CreateDdipRequestDto; -import com.knu.ddip.ddipevent.application.dto.DdipEventDetailDto; -import com.knu.ddip.ddipevent.application.dto.DdipEventSummaryDto; -import com.knu.ddip.ddipevent.application.dto.FeedRequestDto; +import com.knu.ddip.common.dto.StringTypeResponse; +import com.knu.ddip.ddipevent.application.dto.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -34,9 +33,61 @@ ResponseEntity> getDdipEventFeed( @ModelAttribute FeedRequestDto feedRequestDto ); - @Operation(summary = "DDIP Event 상세 조회", description = "ddipId에 해당하는 DDIP Event를 상세 조회한다.") - @GetMapping("/{ddipId}") + @Operation(summary = "DDIP Event 상세 조회", description = "eventId에 해당하는 DDIP Event를 상세 조회한다.") + @GetMapping("/{eventId}") ResponseEntity getDdipEventDetail( - @PathVariable UUID ddipId + @PathVariable UUID eventId + ); + + @Operation(summary = "DDIP event 지원하기", description = "OPEN 상태의 DDIP event에 지원한다.") + @PostMapping("/{eventId}/apply") + @RequireAuth + ResponseEntity applyDdipEvent( + @PathVariable UUID eventId, + @Parameter(hidden = true) @Login AuthUser authUser + ); + + @Operation(summary = "DDIP event 지원자 중 수행자 선택하기", description = "본인이 생성한 DDIP event의 지원자 목록에서 특정 사용자를 선택한다.") + @PostMapping("/{eventId}/select") + @RequireAuth + ResponseEntity selectApplicantForDdipEvent( + @PathVariable UUID eventId, + @RequestBody SelectApplicantRequest selectApplicantRequest, + @Parameter(hidden = true) @Login AuthUser authUser + ); + + @Operation(summary = "수행 증거 사진 제출하기", description = "수행자가 미션 수행 결과 사진을 서버에 업로드한다.") + @PostMapping(value = "/{eventId}/photos", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @RequireAuth + ResponseEntity uploadPhotoForDdipEvent( + @PathVariable UUID eventId, + @ModelAttribute PhotoUploadRequest photoUploadRequest, + @Parameter(hidden = true) @Login AuthUser authUser + ); + + @Operation(summary = "제출된 사진 피드백 업데이트하기", description = "요청자, 수행자 모두 제출된 사진에 대한 피드백을 업데이트한다.") + @PatchMapping("/{eventId}/photos/{photoId}") + @RequireAuth + ResponseEntity updatePhotoFeedback( + @PathVariable(value = "eventId") UUID eventId, + @PathVariable(value = "photoId") UUID photoId, + @RequestBody PhotoFeedbackRequest photoFeedbackRequest, + @Parameter(hidden = true) @Login AuthUser authUser + ); + + @Operation(summary = "미션 최종 완료 처리하기", description = "수행자가 미션 수행을 완료 처리 한다.") + @PostMapping("/{eventId}/complete") + @RequireAuth + ResponseEntity completeDdipEventMission( + @PathVariable UUID eventId, + @Parameter(hidden = true) @Login AuthUser authUser + ); + + @Operation(summary = "미션 중단하기", description = "요청자/수행자가 미션 수행을 중단/포기 처리 한다.") + @PostMapping("/{eventId}/cancel") + @RequireAuth + ResponseEntity cancelDdipEventMission( + @PathVariable UUID eventId, + @Parameter(hidden = true) @Login AuthUser authUser ); } From b5602885d2a95a37f2c3c57d0f42294b9b13643c Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:27:55 +0900 Subject: [PATCH 15/24] =?UTF-8?q?Feat:=20DDIP=20API=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20presentation=EB=8B=A8=20controller=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DdipController.java | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/knu/ddip/ddipevent/presentation/controller/DdipController.java b/src/main/java/com/knu/ddip/ddipevent/presentation/controller/DdipController.java index 043756a..5800389 100644 --- a/src/main/java/com/knu/ddip/ddipevent/presentation/controller/DdipController.java +++ b/src/main/java/com/knu/ddip/ddipevent/presentation/controller/DdipController.java @@ -1,13 +1,12 @@ package com.knu.ddip.ddipevent.presentation.controller; import com.knu.ddip.auth.domain.AuthUser; -import com.knu.ddip.ddipevent.application.dto.CreateDdipRequestDto; -import com.knu.ddip.ddipevent.application.dto.DdipEventDetailDto; -import com.knu.ddip.ddipevent.application.dto.DdipEventSummaryDto; -import com.knu.ddip.ddipevent.application.dto.FeedRequestDto; +import com.knu.ddip.common.dto.StringTypeResponse; +import com.knu.ddip.ddipevent.application.dto.*; import com.knu.ddip.ddipevent.application.service.DdipService; import com.knu.ddip.ddipevent.presentation.api.DdipApi; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -33,8 +32,44 @@ public ResponseEntity> getDdipEventFeed(FeedRequestDto } @Override - public ResponseEntity getDdipEventDetail(UUID ddipId) { - DdipEventDetailDto ddipDetail = ddipService.getDdipEventDetail(ddipId); + public ResponseEntity getDdipEventDetail(UUID eventId) { + DdipEventDetailDto ddipDetail = ddipService.getDdipEventDetail(eventId); return ResponseEntity.ok(ddipDetail); } + + @Override + public ResponseEntity applyDdipEvent(UUID eventId, AuthUser authUser) { + ddipService.applyDdipEvent(eventId, authUser.getId()); + return ResponseEntity.status(HttpStatus.OK).body(new StringTypeResponse("정상적으로 지원되었습니다.")); + } + + @Override + public ResponseEntity selectApplicantForDdipEvent(UUID eventId, SelectApplicantRequest selectApplicantRequest, AuthUser authUser) { + ddipService.selectApplicantForDdipEvent(eventId, selectApplicantRequest, authUser.getId()); + return ResponseEntity.status(HttpStatus.OK).body(new StringTypeResponse("정상적으로 수행자를 선택하였습니다.")); + } + + @Override + public ResponseEntity uploadPhotoForDdipEvent(UUID eventId, PhotoUploadRequest photoUploadRequest, AuthUser authUser) { + DdipEventDetailDto ddipEventDetailDto = ddipService.uploadPhotoForDdipEvent(eventId, photoUploadRequest, authUser.getId()); + return ResponseEntity.ok(ddipEventDetailDto); + } + + @Override + public ResponseEntity updatePhotoFeedback(UUID eventId, UUID photoId, PhotoFeedbackRequest photoFeedbackRequest, AuthUser authUser) { + DdipEventDetailDto ddipEventDetailDto = ddipService.updatePhotoFeedback(eventId, photoId, photoFeedbackRequest, authUser.getId()); + return ResponseEntity.ok(ddipEventDetailDto); + } + + @Override + public ResponseEntity completeDdipEventMission(UUID eventId, AuthUser authUser) { + DdipEventDetailDto ddipEventDetailDto = ddipService.completeDdipEventMission(eventId, authUser.getId()); + return ResponseEntity.ok(ddipEventDetailDto); + } + + @Override + public ResponseEntity cancelDdipEventMission(UUID eventId, AuthUser authUser) { + DdipEventDetailDto ddipEventDetailDto = ddipService.cancelDdipEventMission(eventId, authUser.getId()); + return ResponseEntity.ok(ddipEventDetailDto); + } } From 2dba0ecadb868ad37c4eca60a1f4292cb43aa397 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:28:20 +0900 Subject: [PATCH 16/24] =?UTF-8?q?Chore:=20S3=EC=9A=A9=20properties=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d379d61..95f1230 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,4 +20,10 @@ spring.sql.init.mode=always spring.jpa.defer-datasource-initialization=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect -#spring.jpa.properties.hibernate.default_batch_fetch_size=1000 # TODO: ????. ?? ???? +#spring.jpa.properties.hibernate.default_batch_fetch_size=1000 + +# AWS S3 +cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID} +cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY} +cloud.aws.region.static=ap-northeast-2 +cloud.aws.s3.bucket=${S3_BUCKET_NAME} From 0e8d4a74a46db746918b115c711b7c11af4b8cca Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:28:38 +0900 Subject: [PATCH 17/24] =?UTF-8?q?Test:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/knu/ddip/config/TestEnvironmentConfig.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/com/knu/ddip/config/TestEnvironmentConfig.java b/src/test/java/com/knu/ddip/config/TestEnvironmentConfig.java index be11750..8084c93 100644 --- a/src/test/java/com/knu/ddip/config/TestEnvironmentConfig.java +++ b/src/test/java/com/knu/ddip/config/TestEnvironmentConfig.java @@ -27,6 +27,11 @@ public class TestEnvironmentConfig implements BeforeAllCallback { private static final String TEST_KAKAO_REST_API_KEY = "test_kakao_api_key_for_testing_only"; private static final String TEST_KAKAO_BACKEND_REDIRECT_URI = "http://localhost:8080/auth/oauth/kakao/callback/test"; + // S3 테스트용 설정값 + private static final String AWS_ACCESS_KEY_ID = "AKIA123456789TESTKEY"; + private static final String AWS_SECRET_ACCESS_KEY = "abc123xyz456def789ghi000testKeySecretValue"; + private static final String S3_BUCKET_NAME = "my-test-bucket"; + @Override public void beforeAll(ExtensionContext context) { // JWT 설정 @@ -50,5 +55,10 @@ public void beforeAll(ExtensionContext context) { // Kakao OAuth 설정 System.setProperty("KAKAO_REST_API_KEY", TEST_KAKAO_REST_API_KEY); System.setProperty("KAKAO_BACKEND_REDIRECT_URI", TEST_KAKAO_BACKEND_REDIRECT_URI); + + // S3 설정 + System.setProperty("AWS_ACCESS_KEY_ID", AWS_ACCESS_KEY_ID); + System.setProperty("AWS_SECRET_ACCESS_KEY", AWS_SECRET_ACCESS_KEY); + System.setProperty("S3_BUCKET_NAME", S3_BUCKET_NAME); } } From cd72fd3ef5884eaefe3c89ae9fa99072d64af862 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 12:29:00 +0900 Subject: [PATCH 18/24] =?UTF-8?q?Test:=20=EB=B3=80=EA=B2=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EA=B8=B0=EB=B0=98=20DDIP=20Service=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/DdipServiceTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java index f410ce3..3388870 100644 --- a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java @@ -107,9 +107,9 @@ void givenFeedRequest_whenGetDdipEventFeed_thenListOfDdipEventSummaryDtoIsReturn @Test void givenDdipId_whenGetDdipEventDetail_thenDdipEventDetailDtoIsReturned() { // given - UUID ddipId = UUID.randomUUID(); + UUID eventId = UUID.randomUUID(); DdipEvent ddipEvent = DdipEvent.builder() - .id(ddipId) + .id(eventId) .requesterId(UUID.randomUUID()) .createdAt(Instant.now()) .photos(new ArrayList<>()) @@ -117,14 +117,14 @@ void givenDdipId_whenGetDdipEventDetail_thenDdipEventDetailDtoIsReturned() { .applicants(new ArrayList<>()) .build(); - given(ddipEventRepository.findById(ddipId)).willReturn(Optional.of(ddipEvent)); + given(ddipEventRepository.findById(eventId)).willReturn(Optional.of(ddipEvent)); // when - DdipEventDetailDto result = ddipService.getDdipEventDetail(ddipId); + DdipEventDetailDto result = ddipService.getDdipEventDetail(eventId); // then - assertThat(result.id()).isEqualTo(ddipId.toString()); - verify(ddipEventRepository).findById(ddipId); + assertThat(result.id()).isEqualTo(eventId.toString()); + verify(ddipEventRepository).findById(eventId); } @DisplayName("띱 상세 조회 실패 - 띱을 찾을 수 없음") From 05d046a3c43fd2e17ff87a961dd6ddac78f6b901 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 13:20:32 +0900 Subject: [PATCH 19/24] =?UTF-8?q?Test:=20S3=20file=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=B2=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../S3FileStorageServiceTest.java | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/test/java/com/knu/ddip/common/file/infrastructure/S3FileStorageServiceTest.java diff --git a/src/test/java/com/knu/ddip/common/file/infrastructure/S3FileStorageServiceTest.java b/src/test/java/com/knu/ddip/common/file/infrastructure/S3FileStorageServiceTest.java new file mode 100644 index 0000000..5ca3917 --- /dev/null +++ b/src/test/java/com/knu/ddip/common/file/infrastructure/S3FileStorageServiceTest.java @@ -0,0 +1,222 @@ +package com.knu.ddip.common.file.infrastructure; + +import com.knu.ddip.common.file.FileStorageException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class S3FileStorageServiceTest { + + @InjectMocks + private S3FileStorageService s3FileStorageService; + + @Mock + private S3Client s3Client; + + @Mock + private MultipartFile multipartFile; + + private static final String BUCKET_NAME = "test-bucket"; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(s3FileStorageService, "bucketName", BUCKET_NAME); + } + + @DisplayName("파일 업로드 성공") + @Test + void givenValidFile_whenUploadFile_thenFileUrlIsReturned() throws IOException { + // given + String originalFilename = "test.jpg"; + String contentType = "image/jpeg"; + long fileSize = 1024L; + byte[] fileContent = "test content".getBytes(); + String directory = "photos"; + + given(multipartFile.getOriginalFilename()).willReturn(originalFilename); + given(multipartFile.getContentType()).willReturn(contentType); + given(multipartFile.getSize()).willReturn(fileSize); + given(multipartFile.getInputStream()).willReturn(new ByteArrayInputStream(fileContent)); + given(multipartFile.isEmpty()).willReturn(false); + + PutObjectResponse putObjectResponse = PutObjectResponse.builder().build(); + given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) + .willReturn(putObjectResponse); + + // when + String result = s3FileStorageService.uploadFile(multipartFile, directory); + + // then + assertThat(result).contains(BUCKET_NAME); + assertThat(result).contains(directory); + assertThat(result).startsWith("https://"); + verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + + @DisplayName("파일 업로드 실패 - 빈 파일") + @Test + void givenEmptyFile_whenUploadFile_thenFileStorageExceptionIsThrown() { + // given + given(multipartFile.isEmpty()).willReturn(true); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.uploadFile(multipartFile, "photos")) + .isInstanceOf(FileStorageException.class) + .hasMessageContaining("빈 파일은 업로드할 수 없습니다"); + } + + @DisplayName("파일 업로드 실패 - 파일 크기 초과") + @Test + void givenOversizedFile_whenUploadFile_thenFileStorageExceptionIsThrown() { + // given + long oversizedFileSize = 51 * 1024 * 1024; // 51MB + given(multipartFile.isEmpty()).willReturn(false); + given(multipartFile.getSize()).willReturn(oversizedFileSize); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.uploadFile(multipartFile, "photos")) + .isInstanceOf(FileStorageException.class) + .hasMessageContaining("파일 크기는 50MB를 초과할 수 없습니다"); + } + + @DisplayName("파일 업로드 실패 - 지원하지 않는 파일 타입") + @Test + void givenUnsupportedFileType_whenUploadFile_thenFileStorageExceptionIsThrown() { + // given + given(multipartFile.isEmpty()).willReturn(false); + given(multipartFile.getSize()).willReturn(1024L); + given(multipartFile.getContentType()).willReturn("text/plain"); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.uploadFile(multipartFile, "photos")) + .isInstanceOf(FileStorageException.class) + .hasMessageContaining("이미지 파일만 업로드 가능합니다"); + } + + @DisplayName("파일 업로드 실패 - 콘텐츠 타입이 null") + @Test + void givenNullContentType_whenUploadFile_thenFileStorageExceptionIsThrown() { + // given + given(multipartFile.isEmpty()).willReturn(false); + given(multipartFile.getSize()).willReturn(1024L); + given(multipartFile.getContentType()).willReturn(null); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.uploadFile(multipartFile, "photos")) + .isInstanceOf(FileStorageException.class) + .hasMessageContaining("이미지 파일만 업로드 가능합니다"); + } + + @DisplayName("파일 업로드 실패 - IOException") + @Test + void givenIOException_whenUploadFile_thenFileStorageExceptionIsThrown() throws IOException { + // given + given(multipartFile.isEmpty()).willReturn(false); + given(multipartFile.getOriginalFilename()).willReturn("test.jpg"); + given(multipartFile.getContentType()).willReturn("image/jpeg"); + given(multipartFile.getSize()).willReturn(1024L); + given(multipartFile.getInputStream()).willThrow(new IOException("IO Error")); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.uploadFile(multipartFile, "photos")) + .isInstanceOf(FileStorageException.class) + .hasMessageContaining("파일 업로드에 실패했습니다"); + } + + @DisplayName("파일 업로드 실패 - S3Exception") + @Test + void givenS3Exception_whenUploadFile_thenFileStorageExceptionIsThrown() throws IOException { + // given + given(multipartFile.isEmpty()).willReturn(false); + given(multipartFile.getOriginalFilename()).willReturn("test.jpg"); + given(multipartFile.getContentType()).willReturn("image/jpeg"); + given(multipartFile.getSize()).willReturn(1024L); + given(multipartFile.getInputStream()).willReturn(new ByteArrayInputStream("test".getBytes())); + + given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) + .willThrow(RuntimeException.class); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.uploadFile(multipartFile, "photos")) + .isInstanceOf(RuntimeException.class); + } + + @DisplayName("파일 삭제 성공") + @Test + void givenValidFileUrl_whenDeleteFile_thenFileIsDeleted() { + // given + String fileUrl = "https://test-bucket.s3.amazonaws.com/photos/2023/12/01/test.jpg"; + DeleteObjectResponse deleteObjectResponse = DeleteObjectResponse.builder().build(); + given(s3Client.deleteObject(any(DeleteObjectRequest.class))).willReturn(deleteObjectResponse); + + // when + s3FileStorageService.deleteFile(fileUrl); + + // then + verify(s3Client).deleteObject(any(DeleteObjectRequest.class)); + } + + @DisplayName("파일 삭제 실패 - S3Exception") + @Test + void givenS3Exception_whenDeleteFile_thenFileStorageExceptionIsThrown() { + // given + String fileUrl = "https://test-bucket.s3.amazonaws.com/photos/2023/12/01/test.jpg"; + given(s3Client.deleteObject(any(DeleteObjectRequest.class))) + .willThrow(RuntimeException.class); + + // when & then + assertThatThrownBy(() -> s3FileStorageService.deleteFile(fileUrl)) + .isInstanceOf(RuntimeException.class); + } + + @DisplayName("파일 존재 여부 확인 성공 - 파일 존재함") + @Test + void givenExistingFile_whenExists_thenReturnsTrue() { + // given + String fileUrl = "https://test-bucket.s3.amazonaws.com/photos/2023/12/01/test.jpg"; + HeadObjectResponse headObjectResponse = HeadObjectResponse.builder().build(); + given(s3Client.headObject(any(HeadObjectRequest.class))).willReturn(headObjectResponse); + + // when + boolean result = s3FileStorageService.exists(fileUrl); + + // then + assertThat(result).isTrue(); + verify(s3Client).headObject(any(HeadObjectRequest.class)); + } + + @DisplayName("파일 존재 여부 확인 성공 - 파일 존재하지 않음") + @Test + void givenNonExistingFile_whenExists_thenReturnsFalse() { + // given + String fileUrl = "https://test-bucket.s3.amazonaws.com/photos/2023/12/01/nonexistent.jpg"; + given(s3Client.headObject(any(HeadObjectRequest.class))) + .willThrow(mock(NoSuchKeyException.class)); + + // when + boolean result = s3FileStorageService.exists(fileUrl); + + // then + assertThat(result).isFalse(); + verify(s3Client).headObject(any(HeadObjectRequest.class)); + } +} From 372f5d0d375c3eeb664eb2cf7044f76993026942 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 13:20:53 +0900 Subject: [PATCH 20/24] =?UTF-8?q?Test:=20DDIP=20Service=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/DdipServiceTest.java | 184 +++++++++++++++++- 1 file changed, 178 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java index 27ac5c8..d6e33ec 100644 --- a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java @@ -1,11 +1,8 @@ package com.knu.ddip.ddipevent.application.service; -import com.knu.ddip.ddipevent.application.dto.CreateDdipRequestDto; -import com.knu.ddip.ddipevent.application.dto.DdipEventDetailDto; -import com.knu.ddip.ddipevent.application.dto.DdipEventSummaryDto; -import com.knu.ddip.ddipevent.application.dto.FeedRequestDto; -import com.knu.ddip.ddipevent.domain.DdipEvent; -import com.knu.ddip.ddipevent.domain.DdipStatus; +import com.knu.ddip.common.file.FileStorageService; +import com.knu.ddip.ddipevent.application.dto.*; +import com.knu.ddip.ddipevent.domain.*; import com.knu.ddip.ddipevent.exception.DdipNotFoundException; import com.knu.ddip.ddipevent.application.util.DistanceConverter; import com.knu.ddip.user.business.dto.UserEntityDto; @@ -16,6 +13,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.multipart.MultipartFile; import java.time.Instant; import java.util.ArrayList; @@ -41,6 +39,9 @@ class DdipServiceTest { @Mock private UserRepository userRepository; + @Mock + private FileStorageService fileStorageService; + @Mock private DistanceConverter distanceConverter; @@ -145,4 +146,175 @@ void givenInvalidDdipId_whenGetDdipEventDetail_thenDdipNotFoundExceptionIsThrown .isInstanceOf(DdipNotFoundException.class); verify(ddipEventRepository).findById(invalidDdipId); } + + @DisplayName("사진 업로드 성공") + @Test + void givenPhotoUploadRequest_whenUploadPhotoForDdipEvent_thenPhotoIsUploaded() { + // given + UUID eventId = UUID.randomUUID(); + UUID responderId = UUID.randomUUID(); + MultipartFile mockFile = org.mockito.Mockito.mock(MultipartFile.class); + PhotoUploadRequest request = new PhotoUploadRequest(mockFile, 35.888, 128.61, "업로드 완료"); + String photoUrl = "https://example.com/photo.jpg"; + + UserEntityDto responder = UserEntityDto.builder().id(responderId).build(); + DdipEvent ddipEvent = DdipEvent.builder() + .id(eventId) + .selectedResponderId(responderId) + .status(DdipStatus.IN_PROGRESS) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + DdipEvent updatedEvent = DdipEvent.builder() + .id(eventId) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + + given(userRepository.getById(responderId)).willReturn(responder); + given(ddipEventRepository.findById(eventId)).willReturn(Optional.of(ddipEvent)); + given(fileStorageService.uploadFile(mockFile, "photos")).willReturn(photoUrl); + given(ddipEventRepository.save(any(DdipEvent.class))).willReturn(updatedEvent); + + // when + DdipEventDetailDto result = ddipService.uploadPhotoForDdipEvent(eventId, request, responderId); + + // then + assertThat(result).isNotNull(); + verify(userRepository).getById(responderId); + verify(ddipEventRepository).findById(eventId); + verify(fileStorageService).uploadFile(mockFile, "photos"); + verify(ddipEventRepository).save(any(DdipEvent.class)); + } + + @DisplayName("사진 피드백 업데이트 성공") + @Test + void givenPhotoFeedbackRequest_whenUpdatePhotoFeedback_thenFeedbackIsUpdated() { + // given + UUID eventId = UUID.randomUUID(); + UUID photoId = UUID.randomUUID(); + UUID requesterId = UUID.randomUUID(); + PhotoFeedbackRequest request = new PhotoFeedbackRequest(PhotoStatus.APPROVED, "승인합니다"); + + Photo photo = Photo.builder() + .photoId(photoId) + .status(PhotoStatus.PENDING) + .build(); + + UserEntityDto requester = UserEntityDto.builder().id(requesterId).build(); + DdipEvent ddipEvent = DdipEvent.builder() + .id(eventId) + .requesterId(requesterId) + .photos(List.of(photo)) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + DdipEvent updatedEvent = DdipEvent.builder() + .id(eventId) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + + given(userRepository.getById(requesterId)).willReturn(requester); + given(ddipEventRepository.findById(eventId)).willReturn(Optional.of(ddipEvent)); + given(ddipEventRepository.save(any(DdipEvent.class))).willReturn(updatedEvent); + + // when + DdipEventDetailDto result = ddipService.updatePhotoFeedback(eventId, photoId, request, requesterId); + + // then + assertThat(result).isNotNull(); + verify(userRepository).getById(requesterId); + verify(ddipEventRepository).findById(eventId); + verify(ddipEventRepository).save(any(DdipEvent.class)); + } + + @DisplayName("띱 완료 성공") + @Test + void givenRequesterId_whenCompleteDdipEventMission_thenMissionIsCompleted() { + // given + UUID eventId = UUID.randomUUID(); + UUID requesterId = UUID.randomUUID(); + + Photo approvedPhoto = Photo.builder() + .status(PhotoStatus.APPROVED) + .build(); + + UserEntityDto requester = UserEntityDto.builder().id(requesterId).build(); + DdipEvent ddipEvent = DdipEvent.builder() + .id(eventId) + .requesterId(requesterId) + .photos(List.of(approvedPhoto)) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + DdipEvent updatedEvent = DdipEvent.builder() + .id(eventId) + .status(DdipStatus.COMPLETED) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + + given(userRepository.getById(requesterId)).willReturn(requester); + given(ddipEventRepository.findById(eventId)).willReturn(Optional.of(ddipEvent)); + given(ddipEventRepository.save(any(DdipEvent.class))).willReturn(updatedEvent); + + // when + DdipEventDetailDto result = ddipService.completeDdipEventMission(eventId, requesterId); + + // then + assertThat(result).isNotNull(); + verify(userRepository).getById(requesterId); + verify(ddipEventRepository).findById(eventId); + verify(ddipEventRepository).save(any(DdipEvent.class)); + } + + @DisplayName("띱 취소 성공") + @Test + void givenRequesterOrResponderId_whenCancelDdipEventMission_thenMissionIsCanceled() { + // given + UUID eventId = UUID.randomUUID(); + UUID requesterId = UUID.randomUUID(); + + UserEntityDto requester = UserEntityDto.builder().id(requesterId).build(); + DdipEvent ddipEvent = DdipEvent.builder() + .id(eventId) + .requesterId(requesterId) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + DdipEvent updatedEvent = DdipEvent.builder() + .id(eventId) + .status(DdipStatus.CANCELED) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .applicants(new ArrayList<>()) + .createdAt(Instant.now()) + .build(); + + given(userRepository.getById(requesterId)).willReturn(requester); + given(ddipEventRepository.findById(eventId)).willReturn(Optional.of(ddipEvent)); + given(ddipEventRepository.save(any(DdipEvent.class))).willReturn(updatedEvent); + + // when + DdipEventDetailDto result = ddipService.cancelDdipEventMission(eventId, requesterId); + + // then + assertThat(result).isNotNull(); + verify(userRepository).getById(requesterId); + verify(ddipEventRepository).findById(eventId); + verify(ddipEventRepository).save(any(DdipEvent.class)); + } } From 18373a575483832833ca12bf0b9043a59b6f8432 Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 13:21:02 +0900 Subject: [PATCH 21/24] =?UTF-8?q?Test:=20DDIP=20Event=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddip/ddipevent/domain/DdipEventTest.java | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) diff --git a/src/test/java/com/knu/ddip/ddipevent/domain/DdipEventTest.java b/src/test/java/com/knu/ddip/ddipevent/domain/DdipEventTest.java index 1ffb383..81f10aa 100644 --- a/src/test/java/com/knu/ddip/ddipevent/domain/DdipEventTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/domain/DdipEventTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -164,4 +165,274 @@ void givenNonApplicant_whenSelectResponder_thenDdipBadRequestExceptionIsThrown() assertThatThrownBy(() -> ddipEvent.selectResponder(requesterId, nonApplicantId)) .isInstanceOf(DdipBadRequestException.class); } + + @DisplayName("사진 업로드 성공") + @Test + void givenPhotoInfo_whenUploadPhoto_thenPhotoIsUploaded() { + // given + UUID responderId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .selectedResponderId(responderId) + .status(DdipStatus.IN_PROGRESS) + .photos(new ArrayList<>()) + .interactions(new ArrayList<>()) + .build(); + String photoUrl = "https://example.com/photo.jpg"; + Double latitude = 35.888; + Double longitude = 128.61; + String responderComment = "사진 업로드 완료"; + + // when + ddipEvent.uploadPhoto(responderId, photoUrl, latitude, longitude, responderComment); + + // then + assertThat(ddipEvent.getPhotos()).hasSize(1); + Photo uploadedPhoto = ddipEvent.getPhotos().get(0); + assertThat(uploadedPhoto.getPhotoUrl()).isEqualTo(photoUrl); + assertThat(uploadedPhoto.getLatitude()).isEqualTo(latitude); + assertThat(uploadedPhoto.getLongitude()).isEqualTo(longitude); + assertThat(uploadedPhoto.getResponderComment()).isEqualTo(responderComment); + assertThat(uploadedPhoto.getStatus()).isEqualTo(PhotoStatus.PENDING); + assertThat(ddipEvent.getInteractions()).hasSize(1); + assertThat(ddipEvent.getInteractions().get(0).getActionType()).isEqualTo(ActionType.SUBMIT_PHOTO); + } + + @DisplayName("사진 업로드 실패 - 선택된 수행자가 아님") + @Test + void givenNonSelectedResponder_whenUploadPhoto_thenDdipForbiddenExceptionIsThrown() { + // given + UUID selectedResponderId = UUID.randomUUID(); + UUID otherUserId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .selectedResponderId(selectedResponderId) + .status(DdipStatus.IN_PROGRESS) + .photos(new ArrayList<>()) + .build(); + + // when & then + assertThatThrownBy(() -> ddipEvent.uploadPhoto(otherUserId, "url", 35.888, 128.61, "comment")) + .isInstanceOf(DdipForbiddenException.class); + } + + @DisplayName("사진 피드백 업데이트 성공 - 요청자가 사진 승인") + @Test + void givenRequesterAndApprovedStatus_whenUpdatePhotoFeedback_thenPhotoIsApproved() { + // given + UUID requesterId = UUID.randomUUID(); + UUID photoId = UUID.randomUUID(); + Photo photo = Photo.builder() + .photoId(photoId) + .status(PhotoStatus.PENDING) + .build(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .photos(List.of(photo)) + .interactions(new ArrayList<>()) + .build(); + + // when + ddipEvent.updatePhotoFeedback(requesterId, photoId, PhotoStatus.APPROVED, null); + + // then + assertThat(photo.getStatus()).isEqualTo(PhotoStatus.APPROVED); + } + + @DisplayName("사진 피드백 업데이트 성공 - 요청자가 질문 남김") + @Test + void givenRequesterAndFeedback_whenUpdatePhotoFeedback_thenQuestionIsAdded() { + // given + UUID requesterId = UUID.randomUUID(); + UUID photoId = UUID.randomUUID(); + Photo photo = Photo.builder() + .photoId(photoId) + .status(PhotoStatus.PENDING) + .build(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .photos(List.of(photo)) + .interactions(new ArrayList<>()) + .build(); + String feedback = "사진이 흐릿합니다"; + + // when + ddipEvent.updatePhotoFeedback(requesterId, photoId, PhotoStatus.REJECTED, feedback); + + // then + assertThat(photo.getRequesterQuestion()).isEqualTo(feedback); + assertThat(ddipEvent.getInteractions()).hasSize(1); + assertThat(ddipEvent.getInteractions().get(0).getActionType()).isEqualTo(ActionType.ASK_QUESTION); + } + + @DisplayName("사진 피드백 업데이트 성공 - 수행자가 답변 제공") + @Test + void givenResponderAndFeedback_whenUpdatePhotoFeedback_thenAnswerIsAdded() { + // given + UUID responderId = UUID.randomUUID(); + UUID photoId = UUID.randomUUID(); + Photo photo = Photo.builder() + .photoId(photoId) + .status(PhotoStatus.PENDING) + .requesterQuestion("사진이 흐릿합니다") + .build(); + DdipEvent ddipEvent = DdipEvent.builder() + .selectedResponderId(responderId) + .photos(List.of(photo)) + .interactions(new ArrayList<>()) + .build(); + String answer = "다시 찍어서 올리겠습니다"; + + // when + ddipEvent.updatePhotoFeedback(responderId, photoId, PhotoStatus.REJECTED, answer); + + // then + assertThat(photo.getResponderAnswer()).isEqualTo(answer); + assertThat(ddipEvent.getInteractions()).hasSize(1); + assertThat(ddipEvent.getInteractions().get(0).getActionType()).isEqualTo(ActionType.ANSWER_QUESTION); + } + + @DisplayName("사진 피드백 업데이트 실패 - 권한 없는 사용자") + @Test + void givenUnauthorizedUser_whenUpdatePhotoFeedback_thenDdipBadRequestExceptionIsThrown() { + // given + UUID requesterId = UUID.randomUUID(); + UUID responderId = UUID.randomUUID(); + UUID unauthorizedUserId = UUID.randomUUID(); + UUID photoId = UUID.randomUUID(); + Photo photo = Photo.builder().photoId(photoId).build(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .selectedResponderId(responderId) + .photos(List.of(photo)) + .build(); + + // when & then + assertThatThrownBy(() -> ddipEvent.updatePhotoFeedback(unauthorizedUserId, photoId, PhotoStatus.APPROVED, null)) + .isInstanceOf(DdipBadRequestException.class); + } + + @DisplayName("띱 완료 성공") + @Test + void givenApprovedPhoto_whenComplete_thenDdipEventIsCompleted() { + // given + UUID requesterId = UUID.randomUUID(); + Photo approvedPhoto = Photo.builder() + .status(PhotoStatus.APPROVED) + .build(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .photos(List.of(approvedPhoto)) + .interactions(new ArrayList<>()) + .build(); + + // when + ddipEvent.complete(requesterId); + + // then + assertThat(ddipEvent.getStatus()).isEqualTo(DdipStatus.COMPLETED); + assertThat(ddipEvent.getInteractions()).hasSize(1); + assertThat(ddipEvent.getInteractions().get(0).getActionType()).isEqualTo(ActionType.APPROVE); + } + + @DisplayName("띱 완료 실패 - 요청자가 아님") + @Test + void givenNonRequester_whenComplete_thenDdipForbiddenExceptionIsThrown() { + // given + UUID requesterId = UUID.randomUUID(); + UUID otherUserId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .build(); + + // when & then + assertThatThrownBy(() -> ddipEvent.complete(otherUserId)) + .isInstanceOf(DdipForbiddenException.class); + } + + @DisplayName("띱 완료 실패 - 업로드된 사진 없음") + @Test + void givenNoPhotos_whenComplete_thenDdipBadRequestExceptionIsThrown() { + // given + UUID requesterId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .photos(new ArrayList<>()) + .build(); + + // when & then + assertThatThrownBy(() -> ddipEvent.complete(requesterId)) + .isInstanceOf(DdipBadRequestException.class); + } + + @DisplayName("띱 완료 실패 - 최종 사진이 승인되지 않음") + @Test + void givenUnapprovedLastPhoto_whenComplete_thenDdipForbiddenExceptionIsThrown() { + // given + UUID requesterId = UUID.randomUUID(); + Photo pendingPhoto = Photo.builder() + .status(PhotoStatus.PENDING) + .build(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .photos(List.of(pendingPhoto)) + .build(); + + // when & then + assertThatThrownBy(() -> ddipEvent.complete(requesterId)) + .isInstanceOf(DdipForbiddenException.class); + } + + @DisplayName("띱 취소 성공 - 요청자가 취소") + @Test + void givenRequester_whenCancel_thenDdipEventIsCanceledByRequester() { + // given + UUID requesterId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .interactions(new ArrayList<>()) + .build(); + + // when + ddipEvent.cancel(requesterId); + + // then + assertThat(ddipEvent.getStatus()).isEqualTo(DdipStatus.CANCELED); + assertThat(ddipEvent.getInteractions()).hasSize(1); + assertThat(ddipEvent.getInteractions().get(0).getActionType()).isEqualTo(ActionType.CANCEL_BY_REQUESTER); + } + + @DisplayName("띱 취소 성공 - 수행자가 포기") + @Test + void givenResponder_whenCancel_thenDdipEventIsCanceledByResponder() { + // given + UUID responderId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .selectedResponderId(responderId) + .interactions(new ArrayList<>()) + .build(); + + // when + ddipEvent.cancel(responderId); + + // then + assertThat(ddipEvent.getStatus()).isEqualTo(DdipStatus.CANCELED); + assertThat(ddipEvent.getInteractions()).hasSize(1); + assertThat(ddipEvent.getInteractions().get(0).getActionType()).isEqualTo(ActionType.GIVE_UP_BY_RESPONDER); + } + + @DisplayName("띱 취소 실패 - 권한 없는 사용자") + @Test + void givenUnauthorizedUser_whenCancel_thenDdipBadRequestExceptionIsThrown() { + // given + UUID requesterId = UUID.randomUUID(); + UUID responderId = UUID.randomUUID(); + UUID unauthorizedUserId = UUID.randomUUID(); + DdipEvent ddipEvent = DdipEvent.builder() + .requesterId(requesterId) + .selectedResponderId(responderId) + .build(); + + // when & then + assertThatThrownBy(() -> ddipEvent.cancel(unauthorizedUserId)) + .isInstanceOf(DdipBadRequestException.class); + } } From fe5ec6df31a248d416b562d79a890e48b605ec0b Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 13:21:16 +0900 Subject: [PATCH 22/24] =?UTF-8?q?Test:=20Photo=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knu/ddip/ddipevent/domain/PhotoTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/test/java/com/knu/ddip/ddipevent/domain/PhotoTest.java b/src/test/java/com/knu/ddip/ddipevent/domain/PhotoTest.java index abb180e..73871f1 100644 --- a/src/test/java/com/knu/ddip/ddipevent/domain/PhotoTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/domain/PhotoTest.java @@ -1,5 +1,7 @@ package com.knu.ddip.ddipevent.domain; +import com.knu.ddip.ddipevent.exception.DdipBadRequestException; +import com.knu.ddip.ddipevent.exception.DdipForbiddenException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -7,6 +9,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class PhotoTest { @@ -76,4 +79,109 @@ void givenPendingPhoto_whenReject_thenStatusIsRejected() { // then assertThat(photo.getStatus()).isEqualTo(PhotoStatus.REJECTED); } + + @DisplayName("요청자 피드백 - 첫 번째 질문 성공") + @Test + void givenFirstFeedback_whenFeedbackByRequester_thenQuestionIsSetAndReturnsTrue() { + // given + Photo photo = Photo.builder().status(PhotoStatus.PENDING).build(); + String feedback = "사진이 흐릿합니다"; + + // when + boolean isQuestion = photo.feedbackByRequester(feedback); + + // then + assertThat(isQuestion).isTrue(); + assertThat(photo.getRequesterQuestion()).isEqualTo(feedback); + } + + @DisplayName("요청자 피드백 - 답변 후 거절 성공") + @Test + void givenAnsweredQuestion_whenFeedbackByRequester_thenPhotoIsRejectedAndReturnsFalse() { + // given + Photo photo = Photo.builder() + .status(PhotoStatus.PENDING) + .requesterQuestion("기존 질문") + .responderAnswer("수행자 답변") + .build(); + String rejectionReason = "여전히 만족스럽지 않습니다"; + + // when + boolean isQuestion = photo.feedbackByRequester(rejectionReason); + + // then + assertThat(isQuestion).isFalse(); + assertThat(photo.getStatus()).isEqualTo(PhotoStatus.REJECTED); + assertThat(photo.getRejectionReason()).isEqualTo(rejectionReason); + } + + @DisplayName("요청자 피드백 실패 - 답변 없이 거절 시도") + @Test + void givenUnansweredQuestion_whenFeedbackByRequester_thenDdipBadRequestExceptionIsThrown() { + // given + Photo photo = Photo.builder() + .status(PhotoStatus.PENDING) + .requesterQuestion("기존 질문") + .build(); + String feedback = "거절합니다"; + + // when & then + assertThatThrownBy(() -> photo.feedbackByRequester(feedback)) + .isInstanceOf(DdipBadRequestException.class) + .hasMessageContaining("요청자가 남긴 질문에 대한 수행자의 응답 없이 사진을 반려할 수 없습니다"); + } + + @DisplayName("수행자 피드백 성공") + @Test + void givenQuestionExists_whenFeedbackByResponder_thenAnswerIsSet() { + // given + Photo photo = Photo.builder() + .status(PhotoStatus.PENDING) + .requesterQuestion("사진이 흐릿합니다") + .build(); + String answer = "다시 찍어서 올리겠습니다"; + + // when + photo.feedbackByResponder(answer); + + // then + assertThat(photo.getResponderAnswer()).isEqualTo(answer); + } + + @DisplayName("수행자 피드백 실패 - 질문 없이 답변 시도") + @Test + void givenNoQuestion_whenFeedbackByResponder_thenDdipForbiddenExceptionIsThrown() { + // given + Photo photo = Photo.builder() + .status(PhotoStatus.PENDING) + .build(); + String answer = "답변입니다"; + + // when & then + assertThatThrownBy(() -> photo.feedbackByResponder(answer)) + .isInstanceOf(DdipForbiddenException.class) + .hasMessageContaining("요청자의 질문 없이 질문에 대한 대답을 남길 수 없습니다"); + } + + @DisplayName("사진 상태 확인 - 승인되지 않음") + @Test + void givenNonApprovedPhoto_whenStatusIsNotApproved_thenReturnsTrue() { + // given + Photo pendingPhoto = Photo.builder().status(PhotoStatus.PENDING).build(); + Photo rejectedPhoto = Photo.builder().status(PhotoStatus.REJECTED).build(); + + // when & then + assertThat(pendingPhoto.statusIsNotApproved()).isTrue(); + assertThat(rejectedPhoto.statusIsNotApproved()).isTrue(); + } + + @DisplayName("사진 상태 확인 - 승인됨") + @Test + void givenApprovedPhoto_whenStatusIsNotApproved_thenReturnsFalse() { + // given + Photo approvedPhoto = Photo.builder().status(PhotoStatus.APPROVED).build(); + + // when & then + assertThat(approvedPhoto.statusIsNotApproved()).isFalse(); + } } From a7cccafc65ffcacea190d09bb88e1ae56d7af72b Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 13:21:31 +0900 Subject: [PATCH 23/24] =?UTF-8?q?Test:=20Mapper=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/DdipMapperTest.java | 164 +++++++++++++++++- 1 file changed, 160 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java b/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java index 7bdb7c2..524e880 100644 --- a/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java @@ -1,16 +1,14 @@ package com.knu.ddip.ddipevent.infrastructure; -import com.knu.ddip.ddipevent.domain.DdipEvent; -import com.knu.ddip.ddipevent.domain.Interaction; -import com.knu.ddip.ddipevent.domain.Photo; +import com.knu.ddip.ddipevent.domain.*; import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; import com.knu.ddip.ddipevent.infrastructure.entity.InteractionEntity; import com.knu.ddip.ddipevent.infrastructure.entity.PhotoEntity; -import com.knu.ddip.location.application.util.S2Converter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; import java.util.UUID; @@ -112,4 +110,162 @@ void givenDdipEventEntityWithNullLists_whenToDomain_thenDdipEventDomainIsReturne assertThat(domain.getPhotos()).isNotNull().isEmpty(); assertThat(domain.getInteractions()).isNotNull().isEmpty(); } + + @DisplayName("사진 도메인을 엔티티로 매핑 - 모든 필드 포함") + @Test + void givenPhotoWithAllFields_whenMapToEntity_thenAllFieldsAreMapped() { + // given + UUID photoId = UUID.randomUUID(); + Instant timestamp = Instant.now(); + Photo photo = Photo.builder() + .photoId(photoId) + .photoUrl("https://example.com/photo.jpg") + .latitude(35.888) + .longitude(128.61) + .timestamp(timestamp) + .status(PhotoStatus.PENDING) + .responderComment("수행자 코멘트") + .requesterQuestion("요청자 질문") + .responderAnswer("수행자 답변") + .rejectionReason("거절 사유") + .build(); + + DdipEvent domain = DdipEvent.builder() + .photos(List.of(photo)) + .latitude(0.0) + .longitude(0.0) + .build(); + + // when + DdipEventEntity entity = ddipMapper.toEntity(domain); + + // then + PhotoEntity mappedPhoto = entity.getPhotos().get(0); + assertThat(mappedPhoto.getId()).isEqualTo(photoId); + assertThat(mappedPhoto.getPhotoUrl()).isEqualTo("https://example.com/photo.jpg"); + assertThat(mappedPhoto.getLatitude()).isEqualTo(35.888); + assertThat(mappedPhoto.getLongitude()).isEqualTo(128.61); + assertThat(mappedPhoto.getTimestamp()).isEqualTo(timestamp); + assertThat(mappedPhoto.getStatus()).isEqualTo(PhotoStatus.PENDING); + assertThat(mappedPhoto.getResponderComment()).isEqualTo("수행자 코멘트"); + assertThat(mappedPhoto.getRequesterQuestion()).isEqualTo("요청자 질문"); + assertThat(mappedPhoto.getResponderAnswer()).isEqualTo("수행자 답변"); + assertThat(mappedPhoto.getRejectionReason()).isEqualTo("거절 사유"); + assertThat(mappedPhoto.getDdipEvent()).isEqualTo(entity); + } + + @DisplayName("인터랙션 도메인을 엔티티로 매핑 - 모든 필드 포함") + @Test + void givenInteractionWithAllFields_whenMapToEntity_thenAllFieldsAreMapped() { + // given + UUID interactionId = UUID.randomUUID(); + UUID actorId = UUID.randomUUID(); + UUID relatedPhotoId = UUID.randomUUID(); + Instant timestamp = Instant.now(); + Interaction interaction = Interaction.builder() + .interactionId(interactionId) + .actorId(actorId) + .actorRole(ActorRole.REQUESTER) + .actionType(ActionType.ASK_QUESTION) + .comment("테스트 코멘트") + .relatedPhotoId(relatedPhotoId) + .timestamp(timestamp) + .build(); + + DdipEvent domain = DdipEvent.builder() + .interactions(List.of(interaction)) + .latitude(0.0) + .longitude(0.0) + .build(); + + // when + DdipEventEntity entity = ddipMapper.toEntity(domain); + + // then + InteractionEntity mappedInteraction = entity.getInteractions().get(0); + assertThat(mappedInteraction.getId()).isEqualTo(interactionId); + assertThat(mappedInteraction.getActorId()).isEqualTo(actorId); + assertThat(mappedInteraction.getActorRole()).isEqualTo(ActorRole.REQUESTER); + assertThat(mappedInteraction.getActionType()).isEqualTo(ActionType.ASK_QUESTION); + assertThat(mappedInteraction.getContent()).isEqualTo("테스트 코멘트"); + assertThat(mappedInteraction.getRelatedPhotoId()).isEqualTo(relatedPhotoId); + assertThat(mappedInteraction.getTimestamp()).isEqualTo(timestamp); + assertThat(mappedInteraction.getDdipEvent()).isEqualTo(entity); + } + + @DisplayName("사진 엔티티를 도메인으로 매핑 - 모든 필드 포함") + @Test + void givenPhotoEntityWithAllFields_whenMapToDomain_thenAllFieldsAreMapped() { + // given + UUID photoId = UUID.randomUUID(); + Instant timestamp = Instant.now(); + PhotoEntity photoEntity = PhotoEntity.builder() + .id(photoId) + .photoUrl("https://example.com/photo.jpg") + .latitude(35.888) + .longitude(128.61) + .timestamp(timestamp) + .status(PhotoStatus.APPROVED) + .responderComment("수행자 코멘트") + .requesterQuestion("요청자 질문") + .responderAnswer("수행자 답변") + .rejectionReason("거절 사유") + .build(); + + DdipEventEntity entity = DdipEventEntity.builder() + .photos(List.of(photoEntity)) + .build(); + + // when + DdipEvent domain = ddipMapper.toDomain(entity); + + // then + Photo mappedPhoto = domain.getPhotos().get(0); + assertThat(mappedPhoto.getPhotoId()).isEqualTo(photoId); + assertThat(mappedPhoto.getPhotoUrl()).isEqualTo("https://example.com/photo.jpg"); + assertThat(mappedPhoto.getLatitude()).isEqualTo(35.888); + assertThat(mappedPhoto.getLongitude()).isEqualTo(128.61); + assertThat(mappedPhoto.getTimestamp()).isEqualTo(timestamp); + assertThat(mappedPhoto.getStatus()).isEqualTo(PhotoStatus.APPROVED); + assertThat(mappedPhoto.getResponderComment()).isEqualTo("수행자 코멘트"); + assertThat(mappedPhoto.getRequesterQuestion()).isEqualTo("요청자 질문"); + assertThat(mappedPhoto.getResponderAnswer()).isEqualTo("수행자 답변"); + assertThat(mappedPhoto.getRejectionReason()).isEqualTo("거절 사유"); + } + + @DisplayName("인터랙션 엔티티를 도메인으로 매핑 - 모든 필드 포함") + @Test + void givenInteractionEntityWithAllFields_whenMapToDomain_thenAllFieldsAreMapped() { + // given + UUID interactionId = UUID.randomUUID(); + UUID actorId = UUID.randomUUID(); + UUID relatedPhotoId = UUID.randomUUID(); + Instant timestamp = Instant.now(); + InteractionEntity interactionEntity = InteractionEntity.builder() + .id(interactionId) + .actorId(actorId) + .actorRole(ActorRole.RESPONDER) + .actionType(ActionType.ANSWER_QUESTION) + .content("테스트 코멘트") + .relatedPhotoId(relatedPhotoId) + .timestamp(timestamp) + .build(); + + DdipEventEntity entity = DdipEventEntity.builder() + .interactions(List.of(interactionEntity)) + .build(); + + // when + DdipEvent domain = ddipMapper.toDomain(entity); + + // then + Interaction mappedInteraction = domain.getInteractions().get(0); + assertThat(mappedInteraction.getInteractionId()).isEqualTo(interactionId); + assertThat(mappedInteraction.getActorId()).isEqualTo(actorId); + assertThat(mappedInteraction.getActorRole()).isEqualTo(ActorRole.RESPONDER); + assertThat(mappedInteraction.getActionType()).isEqualTo(ActionType.ANSWER_QUESTION); + assertThat(mappedInteraction.getComment()).isEqualTo("테스트 코멘트"); + assertThat(mappedInteraction.getRelatedPhotoId()).isEqualTo(relatedPhotoId); + assertThat(mappedInteraction.getTimestamp()).isEqualTo(timestamp); + } } From b798cf3f0dea29d28a6222d36713e64b473a58ed Mon Sep 17 00:00:00 2001 From: gitjiho Date: Mon, 8 Sep 2025 13:23:00 +0900 Subject: [PATCH 24/24] =?UTF-8?q?Refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/PhotoFeedbackRequest.java | 4 ++-- .../application/service/DdipService.java | 2 +- .../repository/DdipEventRepositoryImpl.java | 2 +- .../application/service/DdipServiceTest.java | 19 +++++++++++-------- .../infrastructure/DdipMapperTest.java | 8 ++++---- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java b/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java index 662dcd8..2c7089e 100644 --- a/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java +++ b/src/main/java/com/knu/ddip/ddipevent/application/dto/PhotoFeedbackRequest.java @@ -3,7 +3,7 @@ import com.knu.ddip.ddipevent.domain.PhotoStatus; public record PhotoFeedbackRequest( - PhotoStatus status, - String feedback + PhotoStatus status, + String feedback ) { } diff --git a/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java b/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java index e30d77a..175fa2d 100644 --- a/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java +++ b/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java @@ -2,9 +2,9 @@ import com.knu.ddip.common.file.FileStorageService; import com.knu.ddip.ddipevent.application.dto.*; +import com.knu.ddip.ddipevent.application.util.DistanceConverter; import com.knu.ddip.ddipevent.domain.DdipEvent; import com.knu.ddip.ddipevent.exception.DdipNotFoundException; -import com.knu.ddip.ddipevent.application.util.DistanceConverter; import com.knu.ddip.user.business.dto.UserEntityDto; import com.knu.ddip.user.business.service.UserRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java b/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java index 7958c1c..bf1882d 100644 --- a/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java +++ b/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java @@ -1,10 +1,10 @@ package com.knu.ddip.ddipevent.infrastructure.repository; import com.knu.ddip.ddipevent.application.service.DdipEventRepository; +import com.knu.ddip.ddipevent.application.util.DistanceConverter; import com.knu.ddip.ddipevent.domain.DdipEvent; import com.knu.ddip.ddipevent.infrastructure.DdipMapper; import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; -import com.knu.ddip.ddipevent.application.util.DistanceConverter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; diff --git a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java index d6e33ec..3f5f5c4 100644 --- a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java @@ -2,9 +2,12 @@ import com.knu.ddip.common.file.FileStorageService; import com.knu.ddip.ddipevent.application.dto.*; -import com.knu.ddip.ddipevent.domain.*; -import com.knu.ddip.ddipevent.exception.DdipNotFoundException; import com.knu.ddip.ddipevent.application.util.DistanceConverter; +import com.knu.ddip.ddipevent.domain.DdipEvent; +import com.knu.ddip.ddipevent.domain.DdipStatus; +import com.knu.ddip.ddipevent.domain.Photo; +import com.knu.ddip.ddipevent.domain.PhotoStatus; +import com.knu.ddip.ddipevent.exception.DdipNotFoundException; import com.knu.ddip.user.business.dto.UserEntityDto; import com.knu.ddip.user.business.service.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -156,7 +159,7 @@ void givenPhotoUploadRequest_whenUploadPhotoForDdipEvent_thenPhotoIsUploaded() { MultipartFile mockFile = org.mockito.Mockito.mock(MultipartFile.class); PhotoUploadRequest request = new PhotoUploadRequest(mockFile, 35.888, 128.61, "업로드 완료"); String photoUrl = "https://example.com/photo.jpg"; - + UserEntityDto responder = UserEntityDto.builder().id(responderId).build(); DdipEvent ddipEvent = DdipEvent.builder() .id(eventId) @@ -199,12 +202,12 @@ void givenPhotoFeedbackRequest_whenUpdatePhotoFeedback_thenFeedbackIsUpdated() { UUID photoId = UUID.randomUUID(); UUID requesterId = UUID.randomUUID(); PhotoFeedbackRequest request = new PhotoFeedbackRequest(PhotoStatus.APPROVED, "승인합니다"); - + Photo photo = Photo.builder() .photoId(photoId) .status(PhotoStatus.PENDING) .build(); - + UserEntityDto requester = UserEntityDto.builder().id(requesterId).build(); DdipEvent ddipEvent = DdipEvent.builder() .id(eventId) @@ -242,11 +245,11 @@ void givenRequesterId_whenCompleteDdipEventMission_thenMissionIsCompleted() { // given UUID eventId = UUID.randomUUID(); UUID requesterId = UUID.randomUUID(); - + Photo approvedPhoto = Photo.builder() .status(PhotoStatus.APPROVED) .build(); - + UserEntityDto requester = UserEntityDto.builder().id(requesterId).build(); DdipEvent ddipEvent = DdipEvent.builder() .id(eventId) @@ -285,7 +288,7 @@ void givenRequesterOrResponderId_whenCancelDdipEventMission_thenMissionIsCancele // given UUID eventId = UUID.randomUUID(); UUID requesterId = UUID.randomUUID(); - + UserEntityDto requester = UserEntityDto.builder().id(requesterId).build(); DdipEvent ddipEvent = DdipEvent.builder() .id(eventId) diff --git a/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java b/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java index 524e880..65c3afb 100644 --- a/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java @@ -129,7 +129,7 @@ void givenPhotoWithAllFields_whenMapToEntity_thenAllFieldsAreMapped() { .responderAnswer("수행자 답변") .rejectionReason("거절 사유") .build(); - + DdipEvent domain = DdipEvent.builder() .photos(List.of(photo)) .latitude(0.0) @@ -171,7 +171,7 @@ void givenInteractionWithAllFields_whenMapToEntity_thenAllFieldsAreMapped() { .relatedPhotoId(relatedPhotoId) .timestamp(timestamp) .build(); - + DdipEvent domain = DdipEvent.builder() .interactions(List.of(interaction)) .latitude(0.0) @@ -211,7 +211,7 @@ void givenPhotoEntityWithAllFields_whenMapToDomain_thenAllFieldsAreMapped() { .responderAnswer("수행자 답변") .rejectionReason("거절 사유") .build(); - + DdipEventEntity entity = DdipEventEntity.builder() .photos(List.of(photoEntity)) .build(); @@ -250,7 +250,7 @@ void givenInteractionEntityWithAllFields_whenMapToDomain_thenAllFieldsAreMapped( .relatedPhotoId(relatedPhotoId) .timestamp(timestamp) .build(); - + DdipEventEntity entity = DdipEventEntity.builder() .interactions(List.of(interactionEntity)) .build();