diff --git a/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerApi.java b/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerApi.java index 63fc81aca6..3d7a0c9e06 100644 --- a/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerApi.java +++ b/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerApi.java @@ -5,9 +5,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import in.koreatech.koin.domain.owner.dto.OwnerResponse; +import in.koreatech.koin.domain.owner.dto.OwnerUpdateRequest; import in.koreatech.koin.domain.owner.dto.VerifyEmailRequest; import in.koreatech.koin.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; @@ -48,4 +50,21 @@ ResponseEntity requestVerificationToRegister( ResponseEntity getOwner( @Auth(permit = {OWNER}) Long userId ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "사장님 인증용 첨부파일 등록") + @SecurityRequirement(name = "Jwt Authentication") + @PutMapping("/owner") + ResponseEntity putOwner( + @Auth(permit = {OWNER}) Long userId, + @RequestBody @Valid OwnerUpdateRequest request + ); } diff --git a/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerController.java b/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerController.java index 10a710d879..38e54e391e 100644 --- a/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerController.java +++ b/src/main/java/in/koreatech/koin/domain/owner/controller/OwnerController.java @@ -3,11 +3,14 @@ import static in.koreatech.koin.domain.user.model.UserType.OWNER; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; 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 in.koreatech.koin.domain.owner.dto.OwnerResponse; +import in.koreatech.koin.domain.owner.dto.OwnerUpdateRequest; import in.koreatech.koin.domain.owner.dto.VerifyEmailRequest; import in.koreatech.koin.domain.owner.service.OwnerService; import in.koreatech.koin.global.auth.Auth; @@ -29,10 +32,21 @@ public ResponseEntity requestVerificationToRegister( } @Override + @GetMapping("/owner") public ResponseEntity getOwner( @Auth(permit = {OWNER}) Long ownerId ) { OwnerResponse ownerInfo = ownerService.getOwner(ownerId); return ResponseEntity.ok().body(ownerInfo); } + + @Override + @PutMapping("/owner") + public ResponseEntity putOwner( + @Auth(permit = {OWNER}) Long userId, + @RequestBody @Valid OwnerUpdateRequest request + ) { + OwnerResponse ownerInfo = ownerService.putOwner(userId, request); + return ResponseEntity.ok().body(ownerInfo); + } } diff --git a/src/main/java/in/koreatech/koin/domain/owner/dto/OwnerUpdateRequest.java b/src/main/java/in/koreatech/koin/domain/owner/dto/OwnerUpdateRequest.java new file mode 100644 index 0000000000..0773b66110 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/owner/dto/OwnerUpdateRequest.java @@ -0,0 +1,32 @@ +package in.koreatech.koin.domain.owner.dto; + +import java.util.List; + +import org.hibernate.validator.constraints.URL; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +@JsonNaming(SnakeCaseStrategy.class) +public record OwnerUpdateRequest( + @Valid + @Size(min = 3, max = 5, message = "이미지는 사업자등록증, 영업신고증, 통장사본을 포함하여 최소 3개 최대 5개까지 가능합니다.") + @Schema(description = "첨부 파일 URL 목록(최소 3개 최대 5개, 코인 파일 형식이어야 함)") + List attachmentUrls +) { + + @JsonNaming(SnakeCaseStrategy.class) + public record InnerAttachmentUrlRequest( + @NotBlank + @URL(protocol = "https", regexp = ".*static\\.koreatech\\.in.*", message = "코인 파일 저장 형식이 아닙니다.") + @Schema(description = "첨부 파일 URL (코인 파일 형식이어야 함)", example = "https://static.koreatech.in/1.png") + String fileUrl + ) { + + } +} diff --git a/src/main/java/in/koreatech/koin/domain/owner/exception/OwnerAttachmentsCountException.java b/src/main/java/in/koreatech/koin/domain/owner/exception/OwnerAttachmentsCountException.java new file mode 100644 index 0000000000..9aeea3660b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/owner/exception/OwnerAttachmentsCountException.java @@ -0,0 +1,17 @@ +package in.koreatech.koin.domain.owner.exception; + +import in.koreatech.koin.global.exception.ArgumentCountException; + +public class OwnerAttachmentsCountException extends ArgumentCountException { + + private static final String DEFAULT_MESSAGE = "첨부파일 개수가 부족합니다."; + + public OwnerAttachmentsCountException(String message) { + super(message); + } + + public static OwnerAttachmentsCountException withDetail(String detail) { + String message = String.format("%s %s", DEFAULT_MESSAGE, detail); + return new OwnerAttachmentsCountException(message); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/owner/model/OwnerAttachment.java b/src/main/java/in/koreatech/koin/domain/owner/model/OwnerAttachment.java index bfef0b1ada..b89cc50796 100644 --- a/src/main/java/in/koreatech/koin/domain/owner/model/OwnerAttachment.java +++ b/src/main/java/in/koreatech/koin/domain/owner/model/OwnerAttachment.java @@ -68,10 +68,9 @@ public void updateName() { } @Builder - private OwnerAttachment(Owner owner, String url, Boolean isDeleted, String name) { + private OwnerAttachment(Owner owner, String url, Boolean isDeleted) { this.owner = owner; this.url = url; this.isDeleted = isDeleted; - this.name = name; } } diff --git a/src/main/java/in/koreatech/koin/domain/owner/repository/OwnerAttachmentRepository.java b/src/main/java/in/koreatech/koin/domain/owner/repository/OwnerAttachmentRepository.java index 0b3cda1f2e..ce67e8e33c 100644 --- a/src/main/java/in/koreatech/koin/domain/owner/repository/OwnerAttachmentRepository.java +++ b/src/main/java/in/koreatech/koin/domain/owner/repository/OwnerAttachmentRepository.java @@ -1,6 +1,7 @@ package in.koreatech.koin.domain.owner.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.repository.Repository; @@ -13,4 +14,6 @@ public interface OwnerAttachmentRepository extends Repository findAllByOwnerId(Long ownerId); + + Optional findByOwnerIdAndUrl(Long userId, String url); } diff --git a/src/main/java/in/koreatech/koin/domain/owner/service/OwnerService.java b/src/main/java/in/koreatech/koin/domain/owner/service/OwnerService.java index 841f15c774..34766a43fb 100644 --- a/src/main/java/in/koreatech/koin/domain/owner/service/OwnerService.java +++ b/src/main/java/in/koreatech/koin/domain/owner/service/OwnerService.java @@ -9,7 +9,9 @@ import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.domain.owner.dto.OwnerResponse; +import in.koreatech.koin.domain.owner.dto.OwnerUpdateRequest; import in.koreatech.koin.domain.owner.dto.VerifyEmailRequest; +import in.koreatech.koin.domain.owner.exception.OwnerAttachmentsCountException; import in.koreatech.koin.domain.owner.model.Owner; import in.koreatech.koin.domain.owner.model.OwnerAttachment; import in.koreatech.koin.domain.owner.model.OwnerEmailRequestEvent; @@ -31,6 +33,8 @@ @Transactional(readOnly = true) public class OwnerService { + private static final int MIN_ATTACHMENT_COUNT = 3; + private final UserRepository userRepository; private final OwnerInVerificationRepository ownerInVerificationRepository; private final MailService mailService; @@ -66,4 +70,35 @@ public OwnerResponse getOwner(Long ownerId) { List shops = shopRepository.findAllByOwnerId(ownerId); return OwnerResponse.of(foundOwner, attachments, shops); } + + @Transactional + public OwnerResponse putOwner(Long userId, OwnerUpdateRequest request) { + Owner foundOwner = ownerRepository.getById(userId); + List attachments = request.attachmentUrls().stream() + .map(url -> OwnerAttachment.builder() + .owner(foundOwner) + .url(url.fileUrl()) + .isDeleted(false) + .build() + ) + .toList(); + + for (OwnerAttachment attachment : attachments) { + if (ownerAttachmentRepository.findByOwnerIdAndUrl(userId, attachment.getUrl()).isPresent()) { + continue; + } + ownerAttachmentRepository.save(attachment); + } + + List allAttachments = ownerAttachmentRepository.findAllByOwnerId(userId); + validateAttachments(allAttachments); + List shops = shopRepository.findAllByOwnerId(userId); + return OwnerResponse.of(foundOwner, allAttachments, shops); + } + + private void validateAttachments(List allAttachments) { + if (allAttachments.size() < MIN_ATTACHMENT_COUNT) { + throw OwnerAttachmentsCountException.withDetail("attachments size: " + allAttachments.size()); + } + } } diff --git a/src/main/java/in/koreatech/koin/global/exception/ArgumentCountException.java b/src/main/java/in/koreatech/koin/global/exception/ArgumentCountException.java new file mode 100644 index 0000000000..39c485c2eb --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/exception/ArgumentCountException.java @@ -0,0 +1,8 @@ +package in.koreatech.koin.global.exception; + +public class ArgumentCountException extends RuntimeException { + + public ArgumentCountException(String message) { + super(message); + } +} diff --git a/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java b/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java index aa12ea21cc..e9e0f155c9 100644 --- a/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java @@ -2,15 +2,15 @@ import java.time.format.DateTimeParseException; -import in.koreatech.koin.global.auth.exception.AuthException; -import lombok.extern.slf4j.Slf4j; - import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import in.koreatech.koin.global.auth.exception.AuthException; +import lombok.extern.slf4j.Slf4j; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -18,7 +18,8 @@ public class GlobalExceptionHandler { @ExceptionHandler public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.warn(e.getMessage()); - return ResponseEntity.badRequest().body(ErrorResponse.from(e.getMessage())); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ErrorResponse.from("요청 파라미터가 잘못되었습니다.")); } @ExceptionHandler @@ -55,4 +56,11 @@ public ResponseEntity handleDateTimeParseException(DateTimeParseE return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ErrorResponse.from("잘못된 날짜 형식입니다.")); } + + @ExceptionHandler + public ResponseEntity handleArgumentCountException(ArgumentCountException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ErrorResponse.from("요청 파라미터가 잘못되었습니다.")); + } } diff --git a/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java b/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java index 86913ec561..d315167892 100644 --- a/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java @@ -104,7 +104,6 @@ void getOwner() { .when() .get("/owner") .then() - .log().all() .statusCode(HttpStatus.OK.value()) .extract(); @@ -172,4 +171,109 @@ void checkOwnerEventListener() { Mockito.verify(ownerEventListener).onOwnerEmailRequest(event); } + + @Test + @DisplayName("사장님 인증용 이미지를 업로드한다.") + void putOwner() { + // given + User user = User.builder() + .password("1234") + .nickname("주노") + .name("최준호") + .phoneNumber("010-1234-5678") + .userType(OWNER) + .gender(UserGender.MAN) + .email("test@koreatech.ac.kr") + .isAuthed(true) + .isDeleted(false) + .build(); + + Owner ownerRequest = Owner.builder() + .companyRegistrationNumber("123-45-67890") + .companyRegistrationCertificateImageUrl("https://test.com/test.jpg") + .grantShop(true) + .grantEvent(true) + .user(user) + .build(); + + Owner owner = ownerRepository.save(ownerRequest); + + Shop shopRequest = Shop.builder() + .owner(owner) + .name("테스트 상점") + .internalName("테스트") + .chosung("테스트") + .phone("010-1234-5678") + .address("대전광역시 유성구 대학로 291") + .description("테스트 상점입니다.") + .delivery(true) + .deliveryPrice(3000L) + .payCard(true) + .payBank(true) + .isDeleted(false) + .isEvent(false) + .remarks("비고") + .hit(0L) + .build(); + + Shop shop = shopRepository.save(shopRequest); + + String token = jwtProvider.createToken(owner.getUser()); + + // when then + ExtractableResponse response = RestAssured + .given() + .header("Authorization", "Bearer " + token) + .body(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/example/example_image1.png" + }, + { + "file_url": "https://static.koreatech.in/example/example_image2.png" + }, + { + "file_url": "https://static.koreatech.in/example/example_image3.png" + } + ] + } + """) + .contentType(ContentType.JSON) + .when() + .put("/owner") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + assertSoftly( + softly -> { + softly.assertThat(response.body().jsonPath().getString("email")).isEqualTo(user.getEmail()); + softly.assertThat(response.body().jsonPath().getString("name")).isEqualTo(user.getName()); + softly.assertThat(response.body().jsonPath().getString("company_number")) + .isEqualTo(owner.getCompanyRegistrationNumber()); + + softly.assertThat(response.body().jsonPath().getList("attachments").size()).isEqualTo(3); + softly.assertThat(response.body().jsonPath().getLong("attachments[0].id")).isNotNull(); + softly.assertThat(response.body().jsonPath().getString("attachments[0].file_url")) + .isEqualTo("https://static.koreatech.in/example/example_image1.png"); + softly.assertThat(response.body().jsonPath().getString("attachments[0].file_name")) + .isEqualTo("example_image1.png"); + softly.assertThat(response.body().jsonPath().getLong("attachments[1].id")).isNotNull(); + softly.assertThat(response.body().jsonPath().getString("attachments[1].file_url")) + .isEqualTo("https://static.koreatech.in/example/example_image2.png"); + softly.assertThat(response.body().jsonPath().getString("attachments[1].file_name")) + .isEqualTo("example_image2.png"); + softly.assertThat(response.body().jsonPath().getLong("attachments[2].id")).isNotNull(); + softly.assertThat(response.body().jsonPath().getString("attachments[2].file_url")) + .isEqualTo("https://static.koreatech.in/example/example_image3.png"); + softly.assertThat(response.body().jsonPath().getString("attachments[2].file_name")) + .isEqualTo("example_image3.png"); + + softly.assertThat(response.body().jsonPath().getList("shops").size()).isEqualTo(1); + softly.assertThat(response.body().jsonPath().getLong("shops[0].id")).isEqualTo(shop.getId()); + softly.assertThat(response.body().jsonPath().getString("shops[0].name")).isEqualTo(shop.getName()); + } + ); + } }