diff --git a/src/main/java/org/sopt/app/AppApplication.java b/src/main/java/org/sopt/app/AppApplication.java index 2cdd2f2f..890c542d 100644 --- a/src/main/java/org/sopt/app/AppApplication.java +++ b/src/main/java/org/sopt/app/AppApplication.java @@ -3,13 +3,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; @EnableJpaAuditing // JPA Auditing(감시, 감사) 기능을 활성화 하는 어노테이션 createdDate, modifiedDate 저장 활성화 +@EnableAsync @SpringBootApplication public class AppApplication { - public static void main(String[] args) { - SpringApplication.run(AppApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(AppApplication.class, args); + } } diff --git a/src/main/java/org/sopt/app/application/s3/S3Service.java b/src/main/java/org/sopt/app/application/s3/S3Service.java index b6c6562f..3789a1f4 100644 --- a/src/main/java/org/sopt/app/application/s3/S3Service.java +++ b/src/main/java/org/sopt/app/application/s3/S3Service.java @@ -1,5 +1,6 @@ package org.sopt.app.application.s3; +import com.amazonaws.AmazonServiceException; import com.amazonaws.HttpMethod; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; @@ -9,6 +10,7 @@ import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -26,6 +28,7 @@ import org.sopt.app.common.exception.v1.ApiException; import org.sopt.app.common.response.ErrorCode; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -125,4 +128,26 @@ public S3Info.PreSignedUrl getPreSignedUrl(String folderName) { .imageURL(imageURL) .build(); } + + @Async + public void deleteFiles(List fileUrls, String folderName) { + val folderURI = bucket + "/mainpage/makers-app-img/" + folderName; + val fileNameList = getFileNameList(fileUrls); + fileNameList.stream().forEach(file -> deleteFile(folderURI, file)); + } + + private List getFileNameList(List fileUrls) { + return fileUrls.stream().map(url -> { + val fileNameSplit = url.split("/"); + return fileNameSplit[fileNameSplit.length - 1]; + }).collect(Collectors.toList()); + } + + private void deleteFile(String folderURI, String fileName) { + try { + s3Client.deleteObject(folderURI, fileName.replace(File.separatorChar, '/')); + } catch (AmazonServiceException e) { + System.err.println(e.getErrorMessage()); + } + } } diff --git a/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java b/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java new file mode 100644 index 00000000..6f13d827 --- /dev/null +++ b/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java @@ -0,0 +1,18 @@ +package org.sopt.app.application.stamp; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.sopt.app.common.event.Event; + +@Getter +@Setter +public class StampDeletedEvent extends Event { + + private List fileUrls; + + public StampDeletedEvent(List fileUrls) { + super(); + this.fileUrls = fileUrls; + } +} diff --git a/src/main/java/org/sopt/app/application/stamp/StampDeletedEventHandler.java b/src/main/java/org/sopt/app/application/stamp/StampDeletedEventHandler.java new file mode 100644 index 00000000..0a312173 --- /dev/null +++ b/src/main/java/org/sopt/app/application/stamp/StampDeletedEventHandler.java @@ -0,0 +1,18 @@ +package org.sopt.app.application.stamp; + +import lombok.RequiredArgsConstructor; +import org.sopt.app.application.s3.S3Service; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StampDeletedEventHandler { + + private final S3Service s3Service; + + @EventListener(StampDeletedEvent.class) + public void handle(StampDeletedEvent event) { + s3Service.deleteFiles(event.getFileUrls(), "stamp"); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/application/stamp/StampService.java b/src/main/java/org/sopt/app/application/stamp/StampService.java index 04705b99..dec9d91e 100644 --- a/src/main/java/org/sopt/app/application/stamp/StampService.java +++ b/src/main/java/org/sopt/app/application/stamp/StampService.java @@ -3,8 +3,10 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.val; +import org.sopt.app.common.event.Events; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.Stamp; @@ -29,7 +31,15 @@ public class StampService { private final MissionRepository missionRepository; @Transactional(readOnly = true) - public Stamp findStamp(Long userId, Long missionId) { + public Stamp findStamp(StampRequest.FindStampRequest findStampRequest) { + val user = userRepository.findUserByNickname(findStampRequest.getNickname()) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND.getMessage())); + return stampRepository.findByUserIdAndMissionId(user.getId(), findStampRequest.getMissionId()) + .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); + } + + @Transactional(readOnly = true) + public Stamp findStampDeprecated(Long userId, Long missionId) { return stampRepository.findByUserIdAndMissionId(userId, missionId) .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); } @@ -57,16 +67,15 @@ public Stamp uploadStampDeprecated( @Transactional public Stamp uploadStamp( RegisterStampRequest stampRequest, - User user, - Long missionId) { + User user) { - val mission = missionRepository.findById(missionId) + val mission = missionRepository.findById(stampRequest.getMissionId()) .orElseThrow(() -> new BadRequestException(ErrorCode.MISSION_NOT_FOUND.getMessage())); val stamp = Stamp.builder() .contents(stampRequest.getContents()) .createdAt(LocalDateTime.now()) .images(List.of(stampRequest.getImage())) - .missionId(missionId) + .missionId(stampRequest.getMissionId()) .userId(user.getId()) .build(); user.addPoints(mission.getLevel()); @@ -75,7 +84,6 @@ public Stamp uploadStamp( return stampRepository.save(stamp); } - //스탬프 내용 수정 @Transactional public Stamp editStampContentsDeprecated( StampRequest.EditStampRequest editStampRequest, @@ -95,10 +103,9 @@ public Stamp editStampContentsDeprecated( @Transactional public Stamp editStampContents( StampRequest.EditStampRequest editStampRequest, - Long userId, - Long missionId) { + Long userId) { - val stamp = stampRepository.findByUserIdAndMissionId(userId, missionId) + val stamp = stampRepository.findByUserIdAndMissionId(userId, editStampRequest.getMissionId()) .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); if (StringUtils.hasText(editStampRequest.getContents())) { stamp.changeContents(editStampRequest.getContents()); @@ -110,14 +117,19 @@ public Stamp editStampContents( return stampRepository.save(stamp); } - //스탬프 사진 수정 @Transactional public Stamp editStampImagesDeprecated(Stamp stamp, List imgPaths) { stamp.changeImages(imgPaths); return stampRepository.save(stamp); } - //Stamp 삭제 by stampId + @Transactional(readOnly = true) + public void checkDuplicateStamp(Long userId, Long missionId) { + if (stampRepository.findByUserIdAndMissionId(userId, missionId).isPresent()) { + throw new BadRequestException(ErrorCode.DUPLICATE_STAMP.getMessage()); + } + } + @Transactional public void deleteStampById(User user, Long stampId) { @@ -129,14 +141,8 @@ public void deleteStampById(User user, Long stampId) { user.minusPoints(mission.getLevel()); userRepository.save(user); stampRepository.deleteById(stampId); - } - - @Transactional(readOnly = true) - public void checkDuplicateStamp(Long userId, Long missionId) { - if (stampRepository.findByUserIdAndMissionId(userId, missionId).isPresent()) { - throw new BadRequestException(ErrorCode.DUPLICATE_STAMP.getMessage()); - } + Events.raise(new StampDeletedEvent(stamp.getImages())); } @Transactional @@ -144,6 +150,10 @@ public void deleteAllStamps(User user) { stampRepository.deleteAllByUserId(user.getId()); user.initializePoints(); userRepository.save(user); + + val imageUrls = stampRepository.findAllByUserId(user.getId()).stream().map(Stamp::getImages) + .flatMap(images -> images.stream()).collect(Collectors.toList()); + Events.raise(new StampDeletedEvent(imageUrls)); } diff --git a/src/main/java/org/sopt/app/common/config/EventsConfig.java b/src/main/java/org/sopt/app/common/config/EventsConfig.java new file mode 100644 index 00000000..c80912e5 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/EventsConfig.java @@ -0,0 +1,20 @@ +package org.sopt.app.common.config; + +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.event.Events; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class EventsConfig { + + private final ApplicationContext applicationContext; + + @Bean + public InitializingBean eventsInitializer() { + return () -> Events.setPublisher(applicationContext); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/event/Event.java b/src/main/java/org/sopt/app/common/event/Event.java new file mode 100644 index 00000000..9749b56d --- /dev/null +++ b/src/main/java/org/sopt/app/common/event/Event.java @@ -0,0 +1,12 @@ +package org.sopt.app.common.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Event { + +} diff --git a/src/main/java/org/sopt/app/common/event/Events.java b/src/main/java/org/sopt/app/common/event/Events.java new file mode 100644 index 00000000..d8ac2540 --- /dev/null +++ b/src/main/java/org/sopt/app/common/event/Events.java @@ -0,0 +1,20 @@ +package org.sopt.app.common.event; + +import static java.util.Objects.nonNull; + +import org.springframework.context.ApplicationEventPublisher; + +public class Events { + + private static ApplicationEventPublisher publisher; + + public static void setPublisher(ApplicationEventPublisher publisher) { + Events.publisher = publisher; + } + + public static void raise(Object event) { + if (nonNull(publisher)) { + publisher.publishEvent(event); + } + } +} diff --git a/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java b/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java index b3372df7..5d952649 100644 --- a/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java +++ b/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java @@ -1,7 +1,6 @@ package org.sopt.app.common.response; import org.sopt.app.common.exception.BaseException; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -15,10 +14,4 @@ public ResponseEntity onKnownException(BaseException baseException) { baseException.getResponseMessage()), null, baseException.getStatusCode()); } - @ExceptionHandler(value = Exception.class) - public ResponseEntity onException(Exception exception) { - exception.printStackTrace(); - return new ResponseEntity<>(CommonResponse.onFailure(HttpStatus.INTERNAL_SERVER_ERROR, - "서버 에러가 발생했습니다."), null, HttpStatus.INTERNAL_SERVER_ERROR); - } } \ No newline at end of file diff --git a/src/main/java/org/sopt/app/domain/entity/User.java b/src/main/java/org/sopt/app/domain/entity/User.java index b51ba658..70ff4872 100644 --- a/src/main/java/org/sopt/app/domain/entity/User.java +++ b/src/main/java/org/sopt/app/domain/entity/User.java @@ -39,21 +39,18 @@ public class User extends BaseEntity implements UserDetails { private String email; @Column private String clientToken; - @Column private String profileMessage; - @Column private Long points; - @Column @Enumerated(EnumType.STRING) private OsType osType; - @Column(name = "playground_id") + @Column(name = "playground_id", unique = true) private Long playgroundId; - @Column + @Column(name = "playground_token") private String playgroundToken; @Builder diff --git a/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java b/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java index 44b4980c..54c54289 100644 --- a/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java +++ b/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java @@ -24,11 +24,18 @@ public class FirebaseController { public FirebaseResponse.Main getFirebaseInfo() { return FirebaseResponse.Main.builder() - .iosForceUpdateVersion("1.0.0") - .iosAppVersion("1.0.2") + .iosForceUpdateVersion("2.1.0") + .iosAppVersion("2.1.0") .androidForceUpdateVersion("1.0.0") .androidAppVersion("1.0.0") - .notice("안녕하세요, makers입니다. \n 현재 미션 수정/등록이 불가능한 이슈가 확인되어 원인 파악 중에 있습니다. \n 앱 이용에 불편을 드린 점 죄송합니다. \n 빠른 시일 내 복구 후 재공지 드리겠습니다.") + .notice("안녕하세요 32기 여러분들! 드디어 솝트 공식앱 안드로이드/iOS 공식 출시를 했습니다!\n" + + "iOS의 경우에는 기존의 \"솝탬프\" 앱을 업데이트 해주셔야 하고,\n" + + "안드로이드의 경우에는 \"SOPT\" 공식 앱을 구글플레이에서 받아주셔야 합니다.\n" + + "1차 행사때부터 활용 예정이라, **모든 분들이 반드시 솝트 앱을 미리 설치**해주시면 감사하겠습니다.\n" + + "(현재 솝트 앱 접속 후, 솝탬프에서 다른 분들의 솝탬프가 보이지 않는 이슈가 있습니다. 해당 부분은 빠르게 업데이트 예정입니다)\n" + + "또한, 정상적으로 활용하기 위해서는 플레이그라운드 회원 가입이 되어야합니다!\n" + + "혹시 아직 회원 가입이 되지 않았다면, 회원가입 후 플레이그라운드 프로필도 만들어주시길 부탁드리겠습니다.\n" + + "이용에 혹시 이슈가 있다면, 김나연(010-4519-0532)에게 연락주시면 감사하겠습니다") .imgUrl(null) .build(); } diff --git a/src/main/java/org/sopt/app/presentation/stamp/StampController.java b/src/main/java/org/sopt/app/presentation/stamp/StampController.java index 11fa75c7..a031a240 100644 --- a/src/main/java/org/sopt/app/presentation/stamp/StampController.java +++ b/src/main/java/org/sopt/app/presentation/stamp/StampController.java @@ -17,6 +17,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -44,12 +45,27 @@ public class StampController { @ApiResponse(responseCode = "400", description = "no stamp", content = @Content), @ApiResponse(responseCode = "500", description = "server error", content = @Content) }) - @GetMapping("/mission/{missionId}") + @GetMapping("") public ResponseEntity findStampByMissionAndUserId( + @Valid @ModelAttribute StampRequest.FindStampRequest findStampRequest + ) { + val result = stampService.findStamp(findStampRequest); + val response = stampResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + @Operation(summary = "스탬프 조회하기 - DEPRECATED") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "no stamp", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping("/mission/{missionId}") + public ResponseEntity findStampByMissionAndUserIdDeprecated( @AuthenticationPrincipal User user, @PathVariable Long missionId ) { - val result = stampService.findStamp(user.getId(), missionId); + val result = stampService.findStampDeprecated(user.getId(), missionId); val response = stampResponseMapper.of(result); return ResponseEntity.status(HttpStatus.OK).body(response); } @@ -93,14 +109,13 @@ public ResponseEntity editStampDeprecated( @ApiResponse(responseCode = "400", description = "no mission / duplicate stamp", content = @Content), @ApiResponse(responseCode = "500", description = "server error", content = @Content) }) - @PostMapping("/mission/{missionId}") + @PostMapping("") public ResponseEntity registerStamp( @AuthenticationPrincipal User user, - @PathVariable Long missionId, @Valid @RequestBody StampRequest.RegisterStampRequest registerStampRequest ) { - stampService.checkDuplicateStamp(user.getId(), missionId); - val result = stampService.uploadStamp(registerStampRequest, user, missionId); + stampService.checkDuplicateStamp(user.getId(), registerStampRequest.getMissionId()); + val result = stampService.uploadStamp(registerStampRequest, user); val response = stampResponseMapper.of(result); return ResponseEntity.status(HttpStatus.OK).body(response); } @@ -111,13 +126,12 @@ public ResponseEntity registerStamp( @ApiResponse(responseCode = "400", description = "no stamp", content = @Content), @ApiResponse(responseCode = "500", description = "server error", content = @Content) }) - @PutMapping("/mission/{missionId}") + @PutMapping("") public ResponseEntity editStamp( @AuthenticationPrincipal User user, - @PathVariable Long missionId, @Valid @RequestBody StampRequest.EditStampRequest editStampRequest ) { - val stamp = stampService.editStampContents(editStampRequest, user.getId(), missionId); + val stamp = stampService.editStampContents(editStampRequest, user.getId()); val response = stampResponseMapper.of(stamp.getId()); return ResponseEntity.status(HttpStatus.OK).body(response); } @@ -129,8 +143,10 @@ public ResponseEntity editStamp( @ApiResponse(responseCode = "500", description = "server error", content = @Content) }) @DeleteMapping("/{stampId}") - public ResponseEntity deleteStampById(@AuthenticationPrincipal User user, - @PathVariable Long stampId) { + public ResponseEntity deleteStampById( + @AuthenticationPrincipal User user, + @PathVariable Long stampId + ) { stampService.deleteStampById(user, stampId); return ResponseEntity.status(HttpStatus.OK).body(null); } diff --git a/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java b/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java index 9c79c019..1a7f8fa5 100644 --- a/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java +++ b/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java @@ -8,11 +8,27 @@ public class StampRequest { + @Getter + @Setter + @ToString + public static class FindStampRequest { + + @Schema(description = "미션 아이디", example = "1") + @NotNull(message = "missionId may not be null") + private Long missionId; + @Schema(description = "닉네임", example = "스탬프왕") + @NotNull(message = "nickname may not be null") + private String nickname; + } + @Getter @Setter @ToString public static class RegisterStampRequest { + @Schema(description = "미션 아이디", example = "1") + @NotNull(message = "missionId may not be null") + private Long missionId; @Schema(description = "스탬프 이미지", example = "https://s3.ap-northeast-2.amazonaws.com/example/283aab53-22e3-46da-85ec-146c99f82ed4.jpeg") @NotNull(message = "image may not be null") private String image; @@ -26,6 +42,9 @@ public static class RegisterStampRequest { @ToString public static class EditStampRequest { + @Schema(description = "미션 아이디", example = "1") + @NotNull(message = "missionId may not be null") + private Long missionId; @Schema(description = "스탬프 이미지", example = "https://s3.ap-northeast-2.amazonaws.com/example/283aab53-22e3-46da-85ec-146c99f82ed4") @NotNull(message = "image may not be null") private String image;