Skip to content

Commit

Permalink
Feature/dashboard advertisements (#34)
Browse files Browse the repository at this point in the history
* feature: add advertisement entity and register api

* feature: add upload,delete,get api for advertisement

* refactor: change media type for update advertisement
  • Loading branch information
mushroom1324 authored Apr 17, 2024
1 parent 49f74da commit a1a5234
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.repick.domain.advertisement.api;

import com.example.repick.domain.advertisement.dto.AdvertisementResponse;
import com.example.repick.domain.advertisement.dto.PatchAdvertisement;
import com.example.repick.domain.advertisement.dto.PostAdvertisement;
import com.example.repick.domain.advertisement.service.AdvertisementService;
import com.example.repick.global.response.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "Advertisement", description = "광고 관련 API")
@RestController @RequestMapping("/advertisements")
@RequiredArgsConstructor
public class AdvertisementController {

private final AdvertisementService advertisementService;

@Operation(summary = "광고 등록", description = """
광고를 등록합니다.
sequence 값은 광고를 노출할 우선 순위로, 낮을수록 먼저 노출됩니다.
sequence 값이 중복일 경우 에러가 발생합니다. (400: ADVERTISEMENT_SEQUENCE_DUPLICATED)
""")
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public SuccessResponse<AdvertisementResponse> postAdvertisement(@ModelAttribute PostAdvertisement postAdvertisement) {
return SuccessResponse.createSuccess(advertisementService.postAdvertisement(postAdvertisement));
}

@Operation(summary = "광고 순서 수정", description = """
광고 순서를 수정합니다.
sequence 값은 광고를 노출할 우선 순위로, 낮을수록 먼저 노출됩니다.
sequence 값이 중복일 경우 에러가 발생합니다. (400: ADVERTISEMENT_SEQUENCE_DUPLICATED)
- advertisementId: 광고 ID
- sequence: 광고 노출 순서
""")
@PatchMapping
public SuccessResponse<AdvertisementResponse> patchAdvertisement(@RequestBody PatchAdvertisement patchAdvertisement) {
return SuccessResponse.success(advertisementService.patchAdvertisement(patchAdvertisement));
}

@Operation(summary = "광고 삭제", description = """
광고를 삭제합니다. (하드 삭제)
""")
@DeleteMapping("/{advertisementId}")
public SuccessResponse<Boolean> deleteAdvertisement(@PathVariable Long advertisementId) {
return SuccessResponse.success(advertisementService.deleteAdvertisement(advertisementId));
}

@Operation(summary = "광고 조회", description = """
광고를 조회합니다. sequence가 낮은 순서대로 조회됩니다.
""")
@GetMapping
public SuccessResponse<List<AdvertisementResponse>> getAdvertisement() {
return SuccessResponse.success(advertisementService.getAdvertisementList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.repick.domain.advertisement.dto;

import com.example.repick.domain.advertisement.entity.Advertisement;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

public record AdvertisementResponse(
@Schema(description = "광고 ID", example = "1") Long id,
@Schema(description = "광고 이미지 URL") String imageUrl,
@Schema(description = "링크 URL, null일 시 클릭 불가능한 광고입니다.") String linkUrl,
@Schema(description = "광고 노출 순서", example = "1") Integer order

) {


public static AdvertisementResponse of(Advertisement advertisement) {
return new AdvertisementResponse(
advertisement.getId(),
advertisement.getImageUrl(),
advertisement.getLinkUrl(),
advertisement.getSequence()
);
}

public static List<AdvertisementResponse> ofList(List<Advertisement> allByOrderBySequenceAsc) {
return allByOrderBySequenceAsc.stream()
.map(AdvertisementResponse::of)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.repick.domain.advertisement.dto;

import com.example.repick.domain.advertisement.entity.Advertisement;
import io.swagger.v3.oas.annotations.media.Schema;

public record PatchAdvertisement(
@Schema(description = "광고 ID", example = "1") Long advertisementId,
@Schema(description = "광고 노출 순서", example = "1") Integer sequence
) {
public Advertisement toAdvertisement() {
return Advertisement.builder()
.sequence(this.sequence())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.repick.domain.advertisement.dto;

import com.example.repick.domain.advertisement.entity.Advertisement;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.web.multipart.MultipartFile;

public record PostAdvertisement(
@Schema(description = "광고 이미지 파일") MultipartFile image,
@Schema(description = "링크 URL, null일 시 클릭 불가능한 광고입니다.") String linkUrl,
@Schema(description = "광고 노출 순서", example = "1") Integer sequence
) {
public Advertisement toAdvertisement() {
return Advertisement.builder()
.linkUrl(this.linkUrl())
.sequence(this.sequence())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.repick.domain.advertisement.entity;

import com.example.repick.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter
public class Advertisement extends BaseEntity {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String imageUrl;
private String linkUrl; // null일 시 클릭 불가 광고
@Column(nullable = false, unique = true)
private Integer sequence;

@Builder
public Advertisement(String imageUrl, String linkUrl, Integer sequence) {
this.imageUrl = imageUrl;
this.linkUrl = linkUrl;
this.sequence = sequence;
}

public void updateImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}

public void updateSequence(Integer sequence) {
this.sequence = sequence == null ? this.sequence : sequence;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.repick.domain.advertisement.repository;

import com.example.repick.domain.advertisement.entity.Advertisement;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface AdvertisementRepository extends JpaRepository<Advertisement, Long> {
Optional<Advertisement> findBySequence(Integer sequence);

List<Advertisement> findAllByOrderBySequenceAsc();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.example.repick.domain.advertisement.service;

import com.example.repick.domain.advertisement.dto.AdvertisementResponse;
import com.example.repick.domain.advertisement.dto.PatchAdvertisement;
import com.example.repick.domain.advertisement.dto.PostAdvertisement;
import com.example.repick.domain.advertisement.entity.Advertisement;
import com.example.repick.domain.advertisement.repository.AdvertisementRepository;
import com.example.repick.global.aws.S3UploadService;
import com.example.repick.global.error.exception.CustomException;
import com.example.repick.global.error.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@Service @RequiredArgsConstructor
public class AdvertisementService {

private final AdvertisementRepository advertisementRepository;
private final S3UploadService s3UploadService;

private void uploadAndUpdateImage(MultipartFile image, Advertisement advertisement) {
try {
String imageUrl = s3UploadService.saveFile(image, "advertisement/" + advertisement.getId());
advertisement.updateImageUrl(imageUrl);
advertisementRepository.save(advertisement);
}
catch (IOException e) {
e.printStackTrace();
throw new CustomException(ErrorCode.IMAGE_UPLOAD_FAILED);
}
}

public AdvertisementResponse postAdvertisement(PostAdvertisement postAdvertisement) {
Advertisement advertisement = postAdvertisement.toAdvertisement();

advertisementRepository.findBySequence(advertisement.getSequence())
.ifPresent(ad -> {
throw new CustomException(ErrorCode.ADVERTISEMENT_SEQUENCE_DUPLICATED);
});

uploadAndUpdateImage(postAdvertisement.image(), advertisement);

return AdvertisementResponse.of(advertisement);

}

public AdvertisementResponse patchAdvertisement(PatchAdvertisement patchAdvertisement) {
Advertisement advertisement = advertisementRepository.findById(patchAdvertisement.advertisementId())
.orElseThrow(() -> new CustomException(ErrorCode.ADVERTISEMENT_NOT_FOUND));


if (patchAdvertisement.sequence() != null) {
advertisementRepository.findBySequence(patchAdvertisement.sequence())
.ifPresent(ad -> {
throw new CustomException(ErrorCode.ADVERTISEMENT_SEQUENCE_DUPLICATED);
});
}

advertisement.updateSequence(patchAdvertisement.sequence());

advertisementRepository.save(advertisement);

return AdvertisementResponse.of(advertisement);

}

public Boolean deleteAdvertisement(Long advertisementId) {
Advertisement advertisement = advertisementRepository.findById(advertisementId)
.orElseThrow(() -> new CustomException(ErrorCode.ADVERTISEMENT_NOT_FOUND));

advertisementRepository.delete(advertisement);

return true;
}

public List<AdvertisementResponse> getAdvertisementList() {
return AdvertisementResponse.ofList(advertisementRepository.findAllByOrderBySequenceAsc());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ public enum ErrorCode {
// Apple OAuth
APPLE_LOGIN_FAILED(400, "C001", "인증 코드가 올바르지 않거나 만료되었습니다."),
//FCM
USER_FCM_TOKEN_NOT_FOUND(404, "C006", "존재하지 않는 FCM 토큰입니다.");
USER_FCM_TOKEN_NOT_FOUND(404, "C006", "존재하지 않는 FCM 토큰입니다."),
// Advertisement
ADVERTISEMENT_SEQUENCE_DUPLICATED(400, "C001", "중복된 순서의 광고가 존재합니다."),
ADVERTISEMENT_NOT_FOUND(404, "C002", "존재하지 않는 광고입니다.");

private final int status;
private final String code;
Expand Down

0 comments on commit a1a5234

Please sign in to comment.