diff --git a/src/main/java/com/dowe/team/Team.java b/src/main/java/com/dowe/team/Team.java index d32d7cc..f66cc53 100644 --- a/src/main/java/com/dowe/team/Team.java +++ b/src/main/java/com/dowe/team/Team.java @@ -37,16 +37,18 @@ public class Team extends BaseEntity { @Builder public Team( String title, - String description, - String image + String description ) { this.title = title; this.description = description; - this.image = image; } public void assignManagerProfile(Profile profile) { this.managerProfile = profile; } + public void assignImage(String image) { + this.image = image; + } + } diff --git a/src/main/java/com/dowe/team/application/TeamService.java b/src/main/java/com/dowe/team/application/TeamService.java index f3396a0..114de6a 100644 --- a/src/main/java/com/dowe/team/application/TeamService.java +++ b/src/main/java/com/dowe/team/application/TeamService.java @@ -4,6 +4,12 @@ import com.dowe.exception.team.TeamCreationLimitException; import com.dowe.profile.application.ProfileService; +import com.dowe.profile.infrastructure.ProfileRepository; +import com.dowe.team.dto.request.AssignImageRequest; +import com.dowe.team.dto.request.CreateTeamRequest; +import com.dowe.team.dto.response.AssignImageResponse; +import com.dowe.team.dto.response.CreateTeamResponse; +import com.dowe.util.s3.S3PresignedUrlGenerator; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,10 +19,7 @@ import com.dowe.member.infrastructure.MemberRepository; import com.dowe.profile.Profile; import com.dowe.team.Team; -import com.dowe.team.dto.NewTeam; -import com.dowe.team.dto.TeamSettings; import com.dowe.team.infrastructure.TeamRepository; -import com.dowe.util.S3Uploader; import com.dowe.util.StringUtil; import lombok.RequiredArgsConstructor; @@ -26,16 +29,19 @@ @RequiredArgsConstructor public class TeamService { - private static final String TEAM_IMAGE_DIRECTORY = "team"; - private final TeamRepository teamRepository; private final MemberRepository memberRepository; - private final S3Uploader s3Uploader; private final ProfileService profileService; + private final S3PresignedUrlGenerator s3PresignedUrlGenerator; + private final ProfileRepository profileRepository; + @Transactional - public NewTeam create(Long memberId, TeamSettings teamSettings) { + public CreateTeamResponse create( + Long memberId, + CreateTeamRequest request + ) { log.info(">>> TeamService create()"); @@ -43,23 +49,14 @@ public NewTeam create(Long memberId, TeamSettings teamSettings) { .orElseThrow(MemberNotFoundException::new); long profileCount = profileService.countProfiles(member.getId()); - log.info(">>> ProfileService create() profileCount for member {} : {}", memberId, profileCount); if (profileCount == MAXIMUM_TEAM_COUNT) { throw new TeamCreationLimitException(); } - String image = null; - if (teamSettings.getImage() != null && !teamSettings.getImage().isEmpty()) { - image = s3Uploader.upload(TEAM_IMAGE_DIRECTORY, teamSettings.getImage()); - } - - log.info(">>> image: {}", image); - Team team = Team.builder() - .title(StringUtil.removeExtraSpaces(teamSettings.getTitle())) - .description(StringUtil.removeExtraSpaces(teamSettings.getDescription())) - .image(image) + .title(StringUtil.removeExtraSpaces(request.title())) + .description(StringUtil.removeExtraSpaces(request.description())) .build(); Profile defaultManagerProfile = profileService.createDefaultProfile( @@ -69,8 +66,48 @@ public NewTeam create(Long memberId, TeamSettings teamSettings) { team.assignManagerProfile(defaultManagerProfile); + Team savedTeam = teamRepository.save(team); + Long teamId = savedTeam.getId(); + + String presignedUrl = s3PresignedUrlGenerator.generatePresignedUrl( + TEAM_IMAGE_DIRECTORY, + TEAM_IMAGE_PREFIX, + teamId + ); + + return new CreateTeamResponse( + teamId, + presignedUrl + ); + } + + @Transactional + public AssignImageResponse assignImage( + Long memberId, + Long teamId, + AssignImageRequest assignImageRequest + ) { + + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new IllegalArgumentException("Team not found")); + + if (!isTeamManager(team, memberId)) { + throw new IllegalArgumentException("Member is not a manager"); + } + + team.assignImage(assignImageRequest.image()); + teamRepository.save(team); - return new NewTeam(team.getId()); + return new AssignImageResponse(team.getId()); + + } + + private boolean isTeamManager( + Team team, + Long memberId + ) { + Member manager = team.getManagerProfile().getMember(); + return manager.getId().equals(memberId); } } diff --git a/src/main/java/com/dowe/team/dto/request/AssignImageRequest.java b/src/main/java/com/dowe/team/dto/request/AssignImageRequest.java new file mode 100644 index 0000000..db9467c --- /dev/null +++ b/src/main/java/com/dowe/team/dto/request/AssignImageRequest.java @@ -0,0 +1,7 @@ +package com.dowe.team.dto.request; + +public record AssignImageRequest( + String image +) { + +} diff --git a/src/main/java/com/dowe/team/dto/request/CreateTeamRequest.java b/src/main/java/com/dowe/team/dto/request/CreateTeamRequest.java new file mode 100644 index 0000000..51fcd46 --- /dev/null +++ b/src/main/java/com/dowe/team/dto/request/CreateTeamRequest.java @@ -0,0 +1,8 @@ +package com.dowe.team.dto.request; + +public record CreateTeamRequest( + String title, + String description +) { + +} diff --git a/src/main/java/com/dowe/team/dto/response/AssignImageResponse.java b/src/main/java/com/dowe/team/dto/response/AssignImageResponse.java new file mode 100644 index 0000000..757c45b --- /dev/null +++ b/src/main/java/com/dowe/team/dto/response/AssignImageResponse.java @@ -0,0 +1,7 @@ +package com.dowe.team.dto.response; + +public record AssignImageResponse( + Long teamId +) { + +} diff --git a/src/main/java/com/dowe/team/dto/response/CreateTeamResponse.java b/src/main/java/com/dowe/team/dto/response/CreateTeamResponse.java new file mode 100644 index 0000000..17298dc --- /dev/null +++ b/src/main/java/com/dowe/team/dto/response/CreateTeamResponse.java @@ -0,0 +1,8 @@ +package com.dowe.team.dto.response; + +public record CreateTeamResponse( + Long teamId, + String presignedUrl +) { + +} diff --git a/src/main/java/com/dowe/team/presentation/TeamController.java b/src/main/java/com/dowe/team/presentation/TeamController.java index 9d9ec52..1bc00f7 100644 --- a/src/main/java/com/dowe/team/presentation/TeamController.java +++ b/src/main/java/com/dowe/team/presentation/TeamController.java @@ -1,21 +1,23 @@ package com.dowe.team.presentation; -import com.dowe.elasticsearch.application.SearchService; +import com.dowe.team.dto.request.AssignImageRequest; +import com.dowe.team.dto.request.CreateTeamRequest; +import com.dowe.team.dto.response.AssignImageResponse; +import com.dowe.team.dto.response.CreateTeamResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -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; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.dowe.team.application.TeamService; -import com.dowe.team.dto.NewTeam; -import com.dowe.team.dto.TeamSettings; import com.dowe.util.api.ApiResponse; import com.dowe.util.api.ResponseResult; import com.dowe.util.resolver.Login; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @Slf4j @@ -26,16 +28,31 @@ public class TeamController { private final TeamService teamService; @PostMapping("/teams") - public ResponseEntity> create( + public ResponseEntity> create( @Login Long memberId, - @ModelAttribute @Valid TeamSettings teamSettings + @RequestBody CreateTeamRequest createTeamRequest ) { log.info(">>> TeamController create()"); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.created( ResponseResult.TEAM_CREATE_SUCCESS, - teamService.create(memberId, teamSettings) + teamService.create(memberId, createTeamRequest) ) ); } + + @PutMapping("/teams/{teamId}/image") + public ResponseEntity> assignImage( + @Login Long memberId, + @PathVariable Long teamId, + @RequestBody AssignImageRequest assignImageRequest + ) { + log.info(">>> TeamController assignImage()"); + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.ok( + ResponseResult.TEAM_IMAGE_ASSIGN_SUCCESS, + teamService.assignImage(memberId, teamId, assignImageRequest) + )); + } + } diff --git a/src/main/java/com/dowe/util/AppConstants.java b/src/main/java/com/dowe/util/AppConstants.java index 51c4c7b..0004677 100644 --- a/src/main/java/com/dowe/util/AppConstants.java +++ b/src/main/java/com/dowe/util/AppConstants.java @@ -27,4 +27,7 @@ public final class AppConstants { public static final int NO_MORE_LAST_HIT_OFFSET = 1; public static final int HAS_MORE_LAST_HIT_OFFSET = 2; + public static final String TEAM_IMAGE_DIRECTORY = "team/"; + public static final String TEAM_IMAGE_PREFIX = "team_"; + } diff --git a/src/main/java/com/dowe/util/api/ResponseResult.java b/src/main/java/com/dowe/util/api/ResponseResult.java index b4819bd..6ab15d5 100644 --- a/src/main/java/com/dowe/util/api/ResponseResult.java +++ b/src/main/java/com/dowe/util/api/ResponseResult.java @@ -18,6 +18,7 @@ public enum ResponseResult { // Team TEAM_CREATE_SUCCESS("팀 생성에 성공했습니다"), + TEAM_IMAGE_ASSIGN_SUCCESS("팀 이미지 저장에 성공했습니다."), // Random RANDOM_TEAMS_SUCCESS("랜덤 팀 조회에 성공했습니다."), diff --git a/src/main/java/com/dowe/util/s3/S3PresignedUrlGenerator.java b/src/main/java/com/dowe/util/s3/S3PresignedUrlGenerator.java new file mode 100644 index 0000000..74121b4 --- /dev/null +++ b/src/main/java/com/dowe/util/s3/S3PresignedUrlGenerator.java @@ -0,0 +1,46 @@ +package com.dowe.util.s3; + +import java.net.URL; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; + +@Component +@RequiredArgsConstructor +public class S3PresignedUrlGenerator { + + private final S3Presigner s3Presigner; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + public String generatePresignedUrl( + String directory, + String imagePrefix, + Long teamId + ) { + + String key = directory + imagePrefix + teamId; + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .acl("public-read") + .build(); + + PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() + .putObjectRequest(putObjectRequest) + .signatureDuration(Duration.ofMinutes(15)) + .build(); + + URL presignedUrl = s3Presigner.presignPutObject(presignRequest).url(); + + return presignedUrl.toString(); + + } + +} diff --git a/src/test/java/com/dowe/team/presentation/TeamControllerTest.java b/src/test/java/com/dowe/team/presentation/TeamControllerTest.java index 06e9d70..2c40bb4 100644 --- a/src/test/java/com/dowe/team/presentation/TeamControllerTest.java +++ b/src/test/java/com/dowe/team/presentation/TeamControllerTest.java @@ -13,14 +13,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import com.dowe.team.dto.request.CreateTeamRequest; +import com.dowe.team.dto.response.CreateTeamResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockMultipartFile; +import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; import com.dowe.RestDocsSupport; -import com.dowe.team.dto.NewTeam; import com.dowe.util.api.ResponseResult; class TeamControllerTest extends RestDocsSupport { @@ -30,19 +31,26 @@ class TeamControllerTest extends RestDocsSupport { void createSuccess() throws Exception { // given String authorizationHeader = BEARER + "accessToken"; - NewTeam newTeam = new NewTeam(20L); - MockMultipartFile image = new MockMultipartFile("image", "test-image.png", "image/png", "some-image".getBytes()); + + CreateTeamRequest createTeamRequest = new CreateTeamRequest( + "Sample Team", + "Sample Description" + ); + + CreateTeamResponse createTeamResponse = new CreateTeamResponse( + 20L, + "pre-signed url" + ); given(tokenManager.parse(anyString(), any())) .willReturn(1L); given(teamService.create(anyLong(), any())) - .willReturn(newTeam); + .willReturn(createTeamResponse); // when / then - mockMvc.perform(multipart("/teams") - .file(image) - .param("title", "Sample Team") - .param("description", "This is a sample description.") + mockMvc.perform(post("/teams") + .content(objectMapper.writeValueAsString(createTeamRequest)) + .contentType(MediaType.APPLICATION_JSON) .header(AUTHORIZATION, authorizationHeader) .header(ORIGIN, FRONTEND_DOMAIN) .header(HOST, BACKEND_DOMAIN)) @@ -51,14 +59,15 @@ void createSuccess() throws Exception { .andExpect(jsonPath("$.code").value(HttpStatus.CREATED.value())) .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.getReasonPhrase())) .andExpect(jsonPath("$.result").value(ResponseResult.TEAM_CREATE_SUCCESS.getDescription())) - .andExpect(jsonPath("$.data.teamId").value(newTeam.getTeamId())) + .andExpect(jsonPath("$.data.teamId").value(createTeamResponse.teamId())) .andDo(restDocs.document( responseFields( fieldWithPath("code").type(JsonFieldType.NUMBER).description("코드"), fieldWithPath("status").type(JsonFieldType.STRING).description("상태"), fieldWithPath("result").type(JsonFieldType.STRING).description("결과"), fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), - fieldWithPath("data.teamId").type(JsonFieldType.NUMBER).description("팀 ID") + fieldWithPath("data.teamId").type(JsonFieldType.NUMBER).description("팀 ID"), + fieldWithPath("data.presignedUrl").type(JsonFieldType.STRING).description("Pre-signed URL") ) )); }