Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package hs.kr.backend.devpals.domain.banner.controller;

import hs.kr.backend.devpals.domain.banner.dto.BannerRequest;
import hs.kr.backend.devpals.domain.banner.dto.BannerResponse;
import hs.kr.backend.devpals.domain.banner.service.BannerService;
import hs.kr.backend.devpals.global.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequestMapping("/banner")
@RequiredArgsConstructor
@Tag(name = "Banner API", description = "배너 관련 API")
public class BannerController {

private final BannerService bannerService;

@PostMapping(consumes = "multipart/form-data")
@Operation(
summary = "배너 생성",
description = "이미지, 노출 여부, 노출 방식(상시/기간) 및 기간(선택)을 포함하여 배너를 생성합니다."
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "배너 생성 성공")
public ResponseEntity<ApiResponse<BannerResponse>> createBanner(
@RequestPart BannerRequest request,
@RequestPart MultipartFile image
) {
return bannerService.createBanner(request, image);
}

@PatchMapping(value = "/{bannerId}", consumes = "multipart/form-data")
@Operation(
summary = "배너 수정",
description = "배너 ID를 기반으로 기존 배너 정보를 이미지 포함 수정합니다."
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "배너 수정 성공")
public ResponseEntity<ApiResponse<BannerResponse>> updateBanner(
@Parameter(description = "수정할 배너 ID") @PathVariable Long bannerId,
@RequestPart BannerRequest request,
@RequestPart(required = false) MultipartFile image
) {
return bannerService.updateBanner(bannerId, request, image);
}

@DeleteMapping("/{bannerId}")
@Operation(
summary = "배너 삭제",
description = "배너 ID를 기반으로 배너를 삭제합니다."
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "배너 삭제 성공")
public ResponseEntity<ApiResponse<Void>> deleteBanner(
@Parameter(description = "삭제할 배너 ID") @PathVariable Long bannerId
) {
return bannerService.deleteBanner(bannerId);
}

@GetMapping
@Operation(
summary = "전체 배너 조회",
description = "관리자 페이지 또는 시스템용 전체 배너 목록을 조회합니다."
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "전체 배너 조회 성공")
public ResponseEntity<ApiResponse<List<BannerResponse>>> getAllBanners() {
return bannerService.getAllBanners();
}

@GetMapping("/visible")
@Operation(
summary = "노출 중 배너 조회",
description = "isVisible=true인 노출 중인 배너만 필터링하여 조회합니다."
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "노출 중 배너 조회 성공")
public ResponseEntity<ApiResponse<List<BannerResponse>>> getVisibleBanners() {
return bannerService.getVisibleBanners();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package hs.kr.backend.devpals.domain.banner.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import hs.kr.backend.devpals.domain.banner.entity.BannerEntity;
import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BannerRequest {

private String imageUrl;
private boolean isVisible;
private boolean isAlways;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endDate;

public BannerEntity toEntity() {
return BannerEntity.builder()
.imageUrl(imageUrl)
.isVisible(isVisible)
.isAlways(isAlways)
.startDate(isAlways ? null : startDate)
.endDate(isAlways ? null : endDate)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package hs.kr.backend.devpals.domain.banner.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import hs.kr.backend.devpals.domain.banner.entity.BannerEntity;
import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BannerResponse {

private Long id;
private String imageUrl;
private boolean isVisible;
private boolean isAlways;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endDate;

public static BannerResponse from(BannerEntity banner) {
return BannerResponse.builder()
.id(banner.getId())
.imageUrl(banner.getImageUrl())
.isVisible(banner.isVisible())
.isAlways(banner.isAlways())
.startDate(banner.getStartDate())
.endDate(banner.getEndDate())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package hs.kr.backend.devpals.domain.banner.entity;

import hs.kr.backend.devpals.domain.banner.dto.BannerRequest;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "banner")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BannerEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String imageUrl;

@Column(nullable = false)
private boolean isVisible;

@Column(nullable = false)
private boolean isAlways;

private LocalDateTime startDate;

private LocalDateTime endDate;

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();

@Column(nullable = false)
private LocalDateTime updatedAt = LocalDateTime.now();

public void update(BannerRequest request, String newImageUrl) {
if (newImageUrl != null) {
this.imageUrl = newImageUrl;
}
this.isVisible = request.isVisible();
this.isAlways = request.isAlways();
this.startDate = request.isAlways() ? null : request.getStartDate();
this.endDate = request.isAlways() ? null : request.getEndDate();
this.updatedAt = LocalDateTime.now();
}

public void setInvisible() {
this.isVisible = false;
this.updatedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package hs.kr.backend.devpals.domain.banner.repository;

import hs.kr.backend.devpals.domain.banner.entity.BannerEntity;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;

public interface BannerRepository extends JpaRepository<BannerEntity, Long> {
List<BannerEntity> findAllByIsVisibleTrue();

List<BannerEntity> findByEndDateBeforeAndIsVisibleTrue(LocalDateTime now);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package hs.kr.backend.devpals.domain.banner.scheduler;

import hs.kr.backend.devpals.domain.banner.entity.BannerEntity;
import hs.kr.backend.devpals.domain.banner.repository.BannerRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class BannerScheduler {
private final BannerRepository bannerRepository;

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
@Transactional
public void updateExpiredBanners() {
LocalDateTime now = LocalDateTime.now();
List<BannerEntity> expiredBanners = bannerRepository.findByEndDateBeforeAndIsVisibleTrue(now);

for (BannerEntity banner : expiredBanners) {
banner.setInvisible();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package hs.kr.backend.devpals.domain.banner.service;

import hs.kr.backend.devpals.domain.banner.dto.BannerRequest;
import hs.kr.backend.devpals.domain.banner.dto.BannerResponse;
import hs.kr.backend.devpals.domain.banner.entity.BannerEntity;
import hs.kr.backend.devpals.domain.banner.repository.BannerRepository;
import hs.kr.backend.devpals.global.common.ApiResponse;
import hs.kr.backend.devpals.global.exception.CustomException;
import hs.kr.backend.devpals.global.exception.ErrorException;
import hs.kr.backend.devpals.infra.aws.AwsS3Client;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Service
@RequiredArgsConstructor
public class BannerService {

private final BannerRepository bannerRepository;
private final AwsS3Client awsS3Client;

@Transactional
public ResponseEntity<ApiResponse<BannerResponse>> createBanner(BannerRequest request, MultipartFile image) {
if (image == null || image.isEmpty()) {
throw new CustomException(ErrorException.FILE_EMPTY);
}

BannerEntity banner = bannerRepository.save(request.toEntity());

String fileName = "devpals_banner_" + banner.getId();
String imageUrl = awsS3Client.upload(image, fileName);

banner.update(request, imageUrl);

return ResponseEntity.ok(new ApiResponse<>(200, true, "배너 생성 성공", BannerResponse.from(banner)));
}

@Transactional
public ResponseEntity<ApiResponse<BannerResponse>> updateBanner(Long id, BannerRequest request, MultipartFile image) {
BannerEntity banner = bannerRepository.findById(id)
.orElseThrow(() -> new CustomException(ErrorException.BANNER_NOT_FOUND));

String imageUrl = null;

if (image != null && !image.isEmpty()) {
String fileName = "devpals_banner_" + banner.getId();
awsS3Client.delete(fileName);
imageUrl = awsS3Client.upload(image, fileName);
}

banner.update(request, imageUrl);

return ResponseEntity.ok(new ApiResponse<>(200, true, "배너 수정 성공", BannerResponse.from(banner)));
}

@Transactional
public ResponseEntity<ApiResponse<Void>> deleteBanner(Long id) {
BannerEntity banner = bannerRepository.findById(id)
.orElseThrow(() -> new CustomException(ErrorException.BANNER_NOT_FOUND));

String fileName = "devpals_banner_" + banner.getId();
awsS3Client.delete(fileName);
bannerRepository.delete(banner);

return ResponseEntity.ok(new ApiResponse<>(200, true, "배너 삭제 성공", null));
}

@Transactional(readOnly = true)
public ResponseEntity<ApiResponse<List<BannerResponse>>> getAllBanners() {
List<BannerResponse> list = bannerRepository.findAll().stream()
.map(BannerResponse::from)
.toList();

return ResponseEntity.ok(new ApiResponse<>(200, true, "전체 배너 조회 성공", list));
}

@Transactional(readOnly = true)
public ResponseEntity<ApiResponse<List<BannerResponse>>> getVisibleBanners() {
List<BannerResponse> list = bannerRepository.findAllByIsVisibleTrue().stream()
.map(BannerResponse::from)
.toList();

return ResponseEntity.ok(new ApiResponse<>(200, true, "노출 중인 배너 조회 성공", list));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public enum ErrorException {
NOT_FOUND_NOTICE(404, "공지사항을 찾을 수 없습니다."),
ANSWER_NOT_FOUND(404, "해당 문의에 대한 답변이 존재하지 않습니다."),
REPORT_NOT_FOUND(404, "신고를 찾을 수 없습니다."),
BANNER_NOT_FOUND(404, "배너를 찾을 수 없습니다."),

DUPLICATE_EMAIL(409, "중복된 이메일 입니다."),
DUPLICATE_NICKNAME(409, "중복된 닉네임 입니다."),
Expand Down