diff --git a/src/main/java/com/example/codebase/controller/EventController.java b/src/main/java/com/example/codebase/controller/EventController.java new file mode 100644 index 00000000..3ba3f9b6 --- /dev/null +++ b/src/main/java/com/example/codebase/controller/EventController.java @@ -0,0 +1,126 @@ +package com.example.codebase.controller; + +import com.example.codebase.domain.exhibition.dto.*; +import com.example.codebase.domain.exhibition.service.EventService; +import com.example.codebase.domain.image.service.ImageService; +import com.example.codebase.domain.member.entity.Member; +import com.example.codebase.domain.member.exception.NotFoundMemberException; +import com.example.codebase.job.JobService; +import com.example.codebase.util.SecurityUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.PositiveOrZero; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Tag(name = "Event", description = "이벤트 API") +@RestController +@RequestMapping("/api/events") +@Validated +public class EventController { + + private final EventService eventService; + + private final ImageService imageService; + + private final JobService jobService; + + @Autowired + public EventController(EventService eventService, ImageService imageService, JobService jobService) { + this.eventService = eventService; + this.imageService = imageService; + this.jobService = jobService; + } + + @Operation(summary = "이벤트 생성", description = "이벤트 일정을 생성합니다.") + @PreAuthorize("isAuthenticated()") + @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + public ResponseEntity createEvent( + @RequestPart(value = "dto") @Valid EventCreateDTO dto, + @RequestPart(value = "mediaFiles") List mediaFiles, + @RequestPart(value = "thumbnailFile") MultipartFile thumbnailFile) + throws Exception { + String username = + SecurityUtil.getCurrentUsername().orElseThrow(() -> new RuntimeException("로그인이 필요합니다.")); + + dto.validateDates(); + + Member member = eventService.findMemberByUserName(username); + + if (!member.isSubmitedRoleInformation()) { + throw new RuntimeException("추가정보 입력한 사용자만 이벤트를 생성할 수 있습니다."); + } + + imageService.uploadMedias(dto, mediaFiles); + imageService.uploadThumbnail(dto.getThumbnail(), thumbnailFile); + + EventDetailResponseDTO event= eventService.createEvent(dto, member); + + return new ResponseEntity(event, HttpStatus.CREATED); + } + + @Operation(summary = "이벤트 목록 조회", description = "이벤트 목록을 조회합니다.") + @GetMapping + public ResponseEntity getEvent( + @ModelAttribute @Valid EventSearchDTO eventSearchDTO, + @PositiveOrZero @RequestParam(value = "page", defaultValue = "0") int page, + @PositiveOrZero @RequestParam(value = "size", defaultValue = "10") int size, + @RequestParam(defaultValue = "DESC", required = false) String sortDirection) { + eventSearchDTO.repeatTimeValidity(); + + EventPageInfoResponseDTO dtos = eventService.getEvents(eventSearchDTO, page, size, sortDirection); + return new ResponseEntity(dtos, HttpStatus.OK); + } + + @Operation(summary = "이벤트 상세 조회", description = "이벤트 상세를 조회합니다.") + @GetMapping("/{eventId}") + public ResponseEntity getEventDetail(@PathVariable Long eventId) { + EventDetailResponseDTO eventDetailResponseDTO = eventService.getEventDetail(eventId); + return new ResponseEntity(eventDetailResponseDTO, HttpStatus.OK); + } + + @Operation(summary = "이벤트 수정", description = "이벤트를 수정합니다.") + @PreAuthorize("isAuthenticated()") + @PutMapping("/{eventId}") + public ResponseEntity updateEvnet( + @PathVariable Long eventId, + @RequestBody @Valid EventUpdateDTO dto){ + String username = + SecurityUtil.getCurrentUsername().orElseThrow(() -> new RuntimeException("로그인이 필요합니다.")); + + EventDetailResponseDTO eventDetailResponseDTO = eventService.updateEvent(eventId, dto, username); + + return new ResponseEntity(eventDetailResponseDTO, HttpStatus.OK); + } + + @Operation(summary = "이벤트 삭제", description = "이벤트를 삭제합니다.") + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{eventId}") + public ResponseEntity deleteEvent(@PathVariable Long eventId) { + String username = + SecurityUtil.getCurrentUsername().orElseThrow(() -> new RuntimeException("로그인이 필요합니다.")); + + eventService.deleteEvent(eventId, username); + + return new ResponseEntity("이벤트가 삭제되었습니다.", HttpStatus.OK); + } + + @Operation(summary = "수동 이벤트 크롤링 업데이트", description = "수동으로 공공데이터 포털에서 이벤트를 가져옵니다") + @PreAuthorize("isAuthenticated() AND hasRole('ROLE_ADMIN')") + @PostMapping("/crawling/event") + public ResponseEntity crawlingEvent() { + jobService.getEventListScheduler(); + + return new ResponseEntity("이벤트가 업데이트 되었습니다.", HttpStatus.OK); + } + +} diff --git a/src/main/java/com/example/codebase/controller/ExhibitionController.java b/src/main/java/com/example/codebase/controller/ExhibitionController.java index 12e19ca1..9827c586 100644 --- a/src/main/java/com/example/codebase/controller/ExhibitionController.java +++ b/src/main/java/com/example/codebase/controller/ExhibitionController.java @@ -1,6 +1,7 @@ package com.example.codebase.controller; import com.example.codebase.domain.exhibition.dto.*; +import com.example.codebase.domain.exhibition.service.EventService; import com.example.codebase.domain.exhibition.service.ExhibitionService; import com.example.codebase.domain.image.service.ImageService; import com.example.codebase.job.JobService; @@ -18,6 +19,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.PositiveOrZero; +import java.io.IOException; import java.util.List; @Tag(name = "Exhibition", description = "전시회 API") @@ -29,12 +31,15 @@ public class ExhibitionController { private final ImageService imageService; + private final EventService eventService; + private final JobService jobService; @Autowired - public ExhibitionController(ExhibitionService exhibitionService, ImageService imageService, JobService jobService) { + public ExhibitionController(ExhibitionService exhibitionService, ImageService imageService, EventService eventService, JobService jobService) { this.exhibitionService = exhibitionService; this.imageService = imageService; + this.eventService = eventService; this.jobService = jobService; } @@ -128,14 +133,21 @@ public ResponseEntity deleteEventSchedule( } @Operation(summary = "수동 이벤트 업데이트", description = "수동으로 공공데이터 포털에서 이벤트를 가져옵니다") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("isAuthenticated() AND hasRole('ROLE_ADMIN')") @PostMapping("/crawling/exhibition") - public ResponseEntity crawlingExhibition() { - if (!SecurityUtil.isAdmin()) { - throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); - } + public ResponseEntity crawlingExhibition() throws IOException { jobService.getExhibitionListScheduler(); return new ResponseEntity("이벤트가 업데이트 되었습니다.", HttpStatus.OK); } + + @Operation(summary = "이벤트 스케줄 이동 작업", description = "이벤트 스케줄을 이동합니다.") + @PreAuthorize("isAuthenticated() AND hasRole('ROLE_ADMIN')") + @PostMapping("/move/event-schedule") + public ResponseEntity moveEventSchedule(){ + eventService.moveEventSchedule(); + + return new ResponseEntity("이벤트 스케줄이 이동되었습니다.", HttpStatus.OK); + } + } diff --git a/src/main/java/com/example/codebase/domain/exhibition/crawling/dto/detailExhbitionResponse/XmlDetailExhibitionData.java b/src/main/java/com/example/codebase/domain/exhibition/crawling/dto/detailExhbitionResponse/XmlDetailExhibitionData.java index 8fbdff5d..0c4722c1 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/crawling/dto/detailExhbitionResponse/XmlDetailExhibitionData.java +++ b/src/main/java/com/example/codebase/domain/exhibition/crawling/dto/detailExhbitionResponse/XmlDetailExhibitionData.java @@ -3,6 +3,7 @@ import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; import lombok.Getter; import lombok.Setter; @@ -24,9 +25,30 @@ public class XmlDetailExhibitionData { private String url; private String phone; private String imgUrl; + + @XmlElement(defaultValue = "0.0") private String gpsX; + + @XmlElement(defaultValue = "0.0") private String gpsY; + private String placeUrl; private String placeAddr; private String placeSeq; + + public Double getLatitude() { + try { + return Double.parseDouble(gpsX); + } catch (NumberFormatException e) { + return 0.0; + } + } + + public Double getLongitude() { + try { + return Double.parseDouble(gpsY); + } catch (NumberFormatException e) { + return 0.0; + } + } } diff --git a/src/main/java/com/example/codebase/domain/exhibition/crawling/service/DetailEventCrawlingService.java b/src/main/java/com/example/codebase/domain/exhibition/crawling/service/DetailEventCrawlingService.java index 157835ad..3d19e935 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/crawling/service/DetailEventCrawlingService.java +++ b/src/main/java/com/example/codebase/domain/exhibition/crawling/service/DetailEventCrawlingService.java @@ -1,35 +1,36 @@ package com.example.codebase.domain.exhibition.crawling.service; +import com.amazonaws.services.s3.model.AmazonS3Exception; import com.example.codebase.domain.exhibition.crawling.XmlResponseEntity; import com.example.codebase.domain.exhibition.crawling.dto.detailExhbitionResponse.XmlDetailExhibitionResponse; import com.example.codebase.domain.exhibition.crawling.dto.detailExhbitionResponse.XmlDetailExhibitionData; import com.example.codebase.domain.exhibition.crawling.dto.exhibitionResponse.XmlExhibitionData; -import com.example.codebase.domain.exhibition.entity.EventSchedule; -import com.example.codebase.domain.exhibition.entity.EventType; -import com.example.codebase.domain.exhibition.entity.Exhibition; -import com.example.codebase.domain.exhibition.entity.ExhibitionMedia; +import com.example.codebase.domain.exhibition.entity.*; +import com.example.codebase.domain.exhibition.repository.EventRepository; import com.example.codebase.domain.exhibition.repository.EventScheduleRepository; import com.example.codebase.domain.exhibition.repository.ExhibitionMediaRepository; import com.example.codebase.domain.exhibition.repository.ExhibitionRepository; import com.example.codebase.domain.location.entity.Location; import com.example.codebase.domain.location.repository.LocationRepository; import com.example.codebase.domain.member.entity.Member; +import com.example.codebase.s3.S3Service; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; +import org.springframework.data.util.Pair; +import java.io.IOException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,12 +40,16 @@ public class DetailEventCrawlingService { private RestTemplate restTemplate; private ExhibitionMediaRepository exhibitionMediaRepository; + private S3Service s3Service; + private LocationRepository locationRepository; private ExhibitionRepository exhibitionRepository; private EventScheduleRepository eventScheduleRepository; + private EventRepository eventRepository; + @PersistenceContext private EntityManager entityManager; @@ -55,44 +60,59 @@ public class DetailEventCrawlingService { public DetailEventCrawlingService(RestTemplate restTemplate, LocationRepository locationRepository, ExhibitionRepository exhibitionRepository, - EventScheduleRepository eventScheduleRepository) { + EventScheduleRepository eventScheduleRepository, + EventRepository eventRepository, S3Service s3Service) { this.restTemplate = restTemplate; this.locationRepository = locationRepository; this.exhibitionRepository = exhibitionRepository; this.eventScheduleRepository = eventScheduleRepository; + this.eventRepository = eventRepository; + this.s3Service = s3Service; } - public XmlDetailExhibitionResponse loadAndParseXmlData(XmlExhibitionData xmlExhibitionData) { + public XmlDetailExhibitionResponse loadAndParseXmlData(XmlExhibitionData xmlExhibitionData) throws IOException { XmlResponseEntity xmlResponseEntity = loadXmlDatas(xmlExhibitionData); return parseXmlData(xmlResponseEntity); } - private XmlResponseEntity loadXmlDatas(XmlExhibitionData xmlExhibitionData) { + private XmlResponseEntity loadXmlDatas(XmlExhibitionData xmlExhibitionData) throws IOException { + String currentDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String saveFileName = String.format("event-backup/event-detail-backup/%s/공연상세정보_%d.xml", currentDate, xmlExhibitionData.getSeq()); + + try { + ResponseEntity object = s3Service.getObject(saveFileName); + + String body = new String(Objects.requireNonNull(object.getBody())); + return new XmlResponseEntity(body, HttpStatus.OK); + } catch (AmazonS3Exception e) { + log.info(e.getMessage()); + log.info("S3에 파일이 없습니다. API를 호출합니다."); + + Pair apiResponse = getXmlDetailEventApiResponse(xmlExhibitionData); + + byte[] file = apiResponse.getSecond().getBytes(); + s3Service.saveUploadFile(saveFileName, file); + return apiResponse.getFirst(); + } + } + + private Pair getXmlDetailEventApiResponse(XmlExhibitionData xmlExhibitionData) { String url = String.format("http://www.culture.go.kr/openapi/rest/publicperformancedisplays/d/?serviceKey=%s&seq=%d", serviceKey, xmlExhibitionData.getSeq()); ResponseEntity response = restTemplate.getForEntity(url, String.class); XmlResponseEntity xmlResponseEntity = new XmlResponseEntity(response.getBody(), response.getStatusCode()); xmlResponseEntity.statusCodeCheck(); - return xmlResponseEntity; + return Pair.of(xmlResponseEntity, Objects.requireNonNull(response.getBody())); } private XmlDetailExhibitionResponse parseXmlData(XmlResponseEntity xmlResponseEntity) { XmlDetailExhibitionResponse xmlDetailExhibitionResponse = XmlDetailExhibitionResponse.parse(xmlResponseEntity.getBody()); responseStatusCheck(xmlDetailExhibitionResponse); - checkGpsValue(xmlDetailExhibitionResponse); return xmlDetailExhibitionResponse; } - private void checkGpsValue(XmlDetailExhibitionResponse xmlDetailExhibitionResponse) { - XmlDetailExhibitionData detailExhibitionData = xmlDetailExhibitionResponse.getMsgBody().getDetailExhibitionData(); - if (detailExhibitionData.getGpsX() == null || detailExhibitionData.getGpsY() == null) { - detailExhibitionData.setGpsY("0"); // TODO : GPS 0으로 해도 되는지 확인 - detailExhibitionData.setGpsX("0"); - } - } - public Exhibition createExhibition(XmlDetailExhibitionResponse response, Member admin) { XmlDetailExhibitionData detailExhibitionData = response.getMsgBody().getDetailExhibitionData(); EventType eventType = checkEventType(detailExhibitionData); @@ -118,6 +138,33 @@ public Exhibition createExhibition(XmlDetailExhibitionResponse response, Member return exhibition; } + public Event createEvent(XmlDetailExhibitionResponse response, Member admin) { + XmlDetailExhibitionData detailEventData = response.getMsgBody().getDetailExhibitionData(); + + EventType eventType = checkEventType(detailEventData); + + Location location = findOrCreateLocation(detailEventData); + Event event = findOrCreateEvent(detailEventData, admin); + + if (event.isPersist()) { + event.updateEventIfChanged(detailEventData, location); + return event; + } + + EventMedia eventMedia = EventMedia.from(detailEventData, event); + + event.setType(eventType); + event.addEventMedia(eventMedia); + event.setLocation(location); + + return event; + } + + private Event findOrCreateEvent(XmlDetailExhibitionData eventData, Member admin) { + return eventRepository + .findBySeq(eventData.getSeq()) + .orElseGet(() -> Event.of(eventData, admin)); + } private Exhibition findOrCreateExhibition(XmlDetailExhibitionData perforInfo, Member member) { return exhibitionRepository @@ -129,8 +176,8 @@ private boolean hasChanged(Exhibition exhibition, XmlDetailExhibitionData perfor return exhibition.hasChanged(perforInfo); } - private Exhibition updateExhibition(Exhibition existingExhibition, XmlDetailExhibitionData perforInfo, Member member) { - return existingExhibition.update(perforInfo, member); + private void updateExhibition(Exhibition existingExhibition, XmlDetailExhibitionData perforInfo, Member member) { + existingExhibition.update(perforInfo, member); } private void deleteRelatedData(Exhibition exhibition) { @@ -164,13 +211,12 @@ private EventType checkEventType(XmlDetailExhibitionData perforInfo) { } private Location findOrCreateLocation(XmlDetailExhibitionData perforInfo) { - return locationRepository.findByGpsXAndGpsY(perforInfo.getGpsX(), perforInfo.getGpsY()) - .orElseGet(() -> locationRepository.findByName(perforInfo.getRealmName()) + return locationRepository.findByGpsXAndGpsYOrAddress(perforInfo.getGpsX(), perforInfo.getGpsY(), perforInfo.getPlaceAddr()) .orElseGet(() -> { Location newLocation = Location.from(perforInfo); locationRepository.save(newLocation); return newLocation; - })); + }); } private List makeEventSchedule(XmlDetailExhibitionData perforInfo, Exhibition exhibition, Location location) { diff --git a/src/main/java/com/example/codebase/domain/exhibition/crawling/service/ExhibitionCrawlingService.java b/src/main/java/com/example/codebase/domain/exhibition/crawling/service/ExhibitionCrawlingService.java index 150307f3..f8165bd9 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/crawling/service/ExhibitionCrawlingService.java +++ b/src/main/java/com/example/codebase/domain/exhibition/crawling/service/ExhibitionCrawlingService.java @@ -1,18 +1,26 @@ package com.example.codebase.domain.exhibition.crawling.service; +import com.amazonaws.services.s3.model.AmazonS3Exception; import com.example.codebase.domain.exhibition.crawling.XmlResponseEntity; import com.example.codebase.domain.exhibition.crawling.dto.exhibitionResponse.XmlExhibitionResponse; +import com.example.codebase.s3.S3Service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.util.Pair; +import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.io.IOException; +import java.io.StringReader; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; @Service @Slf4j @@ -22,10 +30,14 @@ public class ExhibitionCrawlingService { @Value("${service.key}") private String serviceKey; + + private S3Service s3Service; + @Autowired public ExhibitionCrawlingService( - RestTemplate restTemplate) { + RestTemplate restTemplate, S3Service s3Service) { this.restTemplate = restTemplate; + this.s3Service = s3Service; } public List loadXmlDatas() { @@ -49,8 +61,29 @@ public List loadXmlDatas() { } } - private XmlExhibitionResponse loadXmlDataForCurrentPage(int currentPage) { + private XmlExhibitionResponse loadXmlDataForCurrentPage(int currentPage) throws IOException { String currentDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String savedFileName = String.format("event-backup/%s/전시공연정보_%s_%d.xml", currentDate, currentDate, currentPage); + try { + ResponseEntity object = s3Service.getObject(savedFileName); + + String body = new String(Objects.requireNonNull(object.getBody())); + return XmlExhibitionResponse.parse(body); + } catch (AmazonS3Exception e) { + log.info(e.getMessage()); + log.info("S3에 파일이 없습니다. 이벤트 목록 조회 API를 호출합니다."); + + Pair xmlExhibitionApiResponse = getXmlExhibitionApiResponse(currentPage, currentDate); + XmlExhibitionResponse xmlResponse = xmlExhibitionApiResponse.getFirst(); + + byte[] file = xmlExhibitionApiResponse.getSecond().getBytes(); + s3Service.saveUploadFile(savedFileName, file); + + return xmlResponse; + } + } + + private Pair getXmlExhibitionApiResponse(int currentPage, String currentDate) { String url = String.format("http://www.culture.go.kr/openapi/rest/publicperformancedisplays/period?RequestTime=20100810:23003422&serviceKey=%s&cPage=%d&row=10&from=%s&sortStdr=1", serviceKey, currentPage, currentDate); ResponseEntity response = restTemplate.getForEntity(url, String.class); @@ -60,7 +93,7 @@ private XmlExhibitionResponse loadXmlDataForCurrentPage(int currentPage) { XmlExhibitionResponse xmlResponse = XmlExhibitionResponse.parse(response.getBody()); xmlResponse.statusCheck(); - return xmlResponse; + return Pair.of(xmlResponse, Objects.requireNonNull(response.getBody())); } } diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java new file mode 100644 index 00000000..ff5acad3 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java @@ -0,0 +1,66 @@ +package com.example.codebase.domain.exhibition.dto; + +import com.example.codebase.domain.exhibition.entity.EventType; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +public class EventCreateDTO { + + @NotBlank(message = "제목은 필수입니다.") + private String title; + + @NotBlank(message = "설명은 필수입니다.") + private String description; + + @Parameter(required = false) + private String price; + + @Parameter(required = false) + @Pattern(regexp = "^(http|https)://.*", message = "웹사이트 주소를 입력해주세요. ") + private String link; + + @NotNull(message = "이벤트 타입은 필수입니다.") + private EventType eventType; + + @NotNull(message = "시작일은 필수입니다.") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @NotNull(message = "종료일은 필수입니다.") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate endDate; + + @Parameter(required = false) + private String detailedSchedule; + + @NotNull(message = "장소는 필수입니다.") + private Long locationId; + + @Parameter(required = false) + private String detailLocation; + + @Valid + private List medias; + + @Valid + private ExhibitionMediaCreateDTO thumbnail; + + public void validateDates() { + if (endDate.isBefore(startDate)) { + throw new RuntimeException("종료일은 시작일보다 빠를 수 없습니다."); + } + } + +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventDetailResponseDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventDetailResponseDTO.java new file mode 100644 index 00000000..b8e79f3d --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventDetailResponseDTO.java @@ -0,0 +1,93 @@ +package com.example.codebase.domain.exhibition.dto; + +import com.example.codebase.domain.exhibition.entity.Event; +import com.example.codebase.domain.exhibition.entity.EventMedia; +import com.example.codebase.domain.exhibition.entity.EventType; +import com.example.codebase.domain.location.dto.LocationResponseDTO; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.time.LocalDate; +import java.util.stream.Collectors; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor +public class EventDetailResponseDTO { + + private Long id; + + private String title; + + private String authorName; + + private String authorUserName; + + private String authorProfileImage; + + private String description; + + private String detailLocation; + + private String price; + + private String link; + + private EventType eventType; + + private ExhibitionMediaResponseDTO thumbnail; + + private List medias; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate endDate; + + private String detailedSchedule; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime createdTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime updatedTime; + + private LocationResponseDTO location; + + public static EventDetailResponseDTO from(Event event) { + List medias = event.getEventMedias(); + + ExhibitionMediaResponseDTO thumbnail = + medias.stream().findFirst().map(ExhibitionMediaResponseDTO::from).orElse(null); + + List exhibitionMediaResponseDTOS = + medias.stream().skip(1).map(ExhibitionMediaResponseDTO::from).collect(Collectors.toList()); + + LocationResponseDTO locationResponseDTO = LocationResponseDTO.from(event.getLocation()); + + return EventDetailResponseDTO.builder() + .id(event.getId()) + .title(event.getTitle()) + .authorName(event.getMember().getName()) + .authorUserName(event.getMember().getUsername()) + .description(event.getDescription()) + .thumbnail(thumbnail) + .medias(exhibitionMediaResponseDTOS) + .startDate(event.getStartDate()) + .endDate(event.getEndDate()) + .detailedSchedule(event.getDetailedSchedule()) + .eventType(event.getType()) + .price(event.getPrice()) + .link(event.getLink()) + .detailLocation(event.getDetailLocation()) + .price(event.getPrice()) + .location(locationResponseDTO) + .createdTime(event.getCreatedTime()) + .updatedTime(event.getUpdatedTime()) + .build(); + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventPageInfoResponseDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventPageInfoResponseDTO.java new file mode 100644 index 00000000..faa126d4 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventPageInfoResponseDTO.java @@ -0,0 +1,24 @@ +package com.example.codebase.domain.exhibition.dto; + +import com.example.codebase.controller.dto.PageInfo; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class EventPageInfoResponseDTO { + + List events; + + PageInfo pageInfo; + + public static EventPageInfoResponseDTO of( + List dtos, PageInfo pageInfo) { + EventPageInfoResponseDTO responseDTO = new EventPageInfoResponseDTO(); + responseDTO.events = dtos; + responseDTO.pageInfo = pageInfo; + return responseDTO; + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventResponseDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventResponseDTO.java new file mode 100644 index 00000000..c8d6912e --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventResponseDTO.java @@ -0,0 +1,61 @@ +package com.example.codebase.domain.exhibition.dto; + +import com.example.codebase.domain.exhibition.entity.Event; +import com.example.codebase.domain.exhibition.entity.EventType; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EventResponseDTO { + + private Long id; + + private String title; + + private String author; + + private ExhibitionPageInfoResponseDTO exhibition; + + private ExhibitionMediaResponseDTO thumbnail; + + private EventType eventType; + + private String location; + + private String detailLocation; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate endDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime createdTime; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime updatedTime; + + + public static EventResponseDTO from(Event event) { + return EventResponseDTO.builder() + .id(event.getId()) + .title(event.getTitle()) + .author(event.getMember().getName()) + .thumbnail(ExhibitionMediaResponseDTO.from(event.getEventMedias().get(0))) + .eventType(event.getType()) + .location(event.getLocation().getAddress()) + .startDate(event.getStartDate()) + .endDate(event.getEndDate()) + .createdTime(event.getCreatedTime()) + .updatedTime(event.getUpdatedTime()) + .build(); + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleCreateDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleCreateDTO.java index ef6842cb..e560fcd5 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleCreateDTO.java +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleCreateDTO.java @@ -14,11 +14,9 @@ public class EventScheduleCreateDTO { @NotNull(message = "시작시간은 필수입니다.") - @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") private LocalDateTime startDateTime; - @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") private LocalDateTime endDateTime; diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleResponseDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleResponseDTO.java index e38f1916..5e3c9ea2 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleResponseDTO.java +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventScheduleResponseDTO.java @@ -26,11 +26,9 @@ public class EventScheduleResponseDTO { private List participants; - @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime startDateTime; - @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime endDateTime; diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventSearchDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventSearchDTO.java new file mode 100644 index 00000000..7994a522 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventSearchDTO.java @@ -0,0 +1,28 @@ +package com.example.codebase.domain.exhibition.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +@Setter +@Getter +public class EventSearchDTO { + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate = LocalDate.of(1900, 1, 1); + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endDate = LocalDate.of(2100, 12, 31); + + @NotBlank(message = "이벤트 타입은 필수입니다.") + private String eventType; + + public void repeatTimeValidity() { + if (this.startDate.isAfter(this.endDate)) { + throw new RuntimeException("시작일은 종료일보다 이전에 있어야 합니다."); + } + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java new file mode 100644 index 00000000..6237f665 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java @@ -0,0 +1,52 @@ +package com.example.codebase.domain.exhibition.dto; + +import com.example.codebase.domain.exhibition.entity.EventType; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +public class EventUpdateDTO { + + @Parameter(required = false) + private String title; + + @Parameter(required = false) + private String description; + + @Parameter(required = false) + private String price; + + @Parameter(required = false) + @Pattern(regexp = "^(http|https)://.*", message = "웹사이트 주소를 입력해주세요. ") + private String link; + + @Parameter(required = false) + private EventType eventType; + + @Parameter(required = false) + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @Parameter(required = false) + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate endDate; + + @Parameter(required = false) + private String detailedSchedule; + + @Parameter(required = false) + private String detailLocation; + + @Parameter(required = false) + private Long locationId; + +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/dto/ExhibitionMediaResponseDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/ExhibitionMediaResponseDTO.java index 66c87168..86af9dfe 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/dto/ExhibitionMediaResponseDTO.java +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/ExhibitionMediaResponseDTO.java @@ -1,5 +1,6 @@ package com.example.codebase.domain.exhibition.dto; +import com.example.codebase.domain.exhibition.entity.EventMedia; import com.example.codebase.domain.exhibition.entity.ExhibitionMedia; import lombok.Getter; import lombok.Setter; @@ -18,4 +19,11 @@ public static ExhibitionMediaResponseDTO from(ExhibitionMedia exhibitionMedia) { dto.setMediaUrl(exhibitionMedia.getMediaUrl()); return dto; } + + public static ExhibitionMediaResponseDTO from(EventMedia eventMedia) { + ExhibitionMediaResponseDTO dto = new ExhibitionMediaResponseDTO(); + dto.setMediaType(eventMedia.getEventMediaType().name()); + dto.setMediaUrl(eventMedia.getMediaUrl()); + return dto; + } } diff --git a/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java b/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java new file mode 100644 index 00000000..4efa271d --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java @@ -0,0 +1,261 @@ +package com.example.codebase.domain.exhibition.entity; + + +import com.example.codebase.domain.exhibition.crawling.dto.detailExhbitionResponse.XmlDetailExhibitionData; +import com.example.codebase.domain.exhibition.dto.EventCreateDTO; +import com.example.codebase.domain.exhibition.dto.EventUpdateDTO; +import com.example.codebase.domain.exhibition.dto.ExhbitionCreateDTO; +import com.example.codebase.domain.location.entity.Location; +import com.example.codebase.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@Entity +@Table(name = "event") +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Where(clause = "enabled = true") +public class Event { + + @Id + @Column(name = "event_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @Column(name = "detail_location", columnDefinition = "varchar(255)") + private String detailLocation; + + @Column(name = "price", nullable = false) + private String price; + + @Column(name = "link", length = 200) + private String link; + + @Builder.Default + @Column(name = "type", nullable = false) + @Enumerated(EnumType.STRING) + private EventType type = EventType.STANDARD; + + @Builder.Default + @Column(name = "enabled") + private boolean enabled = true; + + @Column(name = "seq") + private Long seq; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date" , nullable = false) + private LocalDate endDate; + + @Column(name =" detailed_schedule") + private String detailedSchedule; + + @Column(name = "created_time") + private LocalDateTime createdTime; + + @Column(name = "updated_time") + private LocalDateTime updatedTime; + + @ManyToOne + @JoinColumn(name = "member_id", columnDefinition = "BINARY(16)", nullable = false) + private Member member; + + @Builder.Default + @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) + private List eventMedias = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "location_id") + private Location location; + + public static Event from(Exhibition exhibition) { + + if(exhibition.getEventSchedules().isEmpty()){ + return null; + } + + EventSchedule earliestSchedule = exhibition.getEventSchedules().stream() + .min(Comparator.comparing(EventSchedule::getStartDateTime)) + .orElse(null); + + EventSchedule latestSchedule = exhibition.getEventSchedules().stream() + .max(Comparator.comparing(EventSchedule::getStartDateTime)) + .orElse(null); + + Location location = exhibition.getEventSchedules().get(0).getLocation(); + + return Event.builder() + .title(exhibition.getTitle()) + .description(exhibition.getDescription()) + .detailLocation(exhibition.getEventSchedules().get(0).getDetailLocation()) + .price(exhibition.getPrice()) + .link(exhibition.getLink()) + .type(exhibition.getType()) + .enabled(exhibition.getEnabled()) + .seq(exhibition.getSeq()) + .startDate(exhibition.getEventSchedules().get(0).getStartDateTime().toLocalDate()) + .endDate(exhibition.getEventSchedules().get(0).getEndDateTime().toLocalDate()) + .detailedSchedule(exhibition.getEventSchedules().get(0).getDetailLocation()) + + .startDate(earliestSchedule.getStartDateTime().toLocalDate()) + .endDate(latestSchedule.getStartDateTime().toLocalDate()) + + .createdTime(exhibition.getCreatedTime()) + .updatedTime(exhibition.getUpdatedTime() != null ? exhibition.getUpdatedTime() : exhibition.getCreatedTime()) + + .member(exhibition.getMember()) + .location(location) + .build(); + } + + public void setEventMedias(List eventMedias) { + this.eventMedias = eventMedias; + } + + public static Event of(EventCreateDTO dto, Member member, Location location){ + return Event.builder() + .title(dto.getTitle()) + .description(dto.getDescription()) + .detailLocation(dto.getDetailLocation()) + .price(dto.getPrice()) + .link(dto.getLink()) + .type(dto.getEventType()) + .startDate(dto.getStartDate()) + .endDate(dto.getEndDate()) + .detailedSchedule(dto.getDetailedSchedule()) + .member(member) + .createdTime(LocalDateTime.now()) + .updatedTime(LocalDateTime.now()) + .location(location) + .build(); + } + + public static Event of(XmlDetailExhibitionData eventData, Member admin){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + return Event.builder() + .title(eventData.getTitle()) + .description(eventData.getContents1() + "\n" + eventData.getContents2()) + .detailLocation(eventData.getPlace()) + .price(eventData.getPrice()) + .link(eventData.getUrl()) + .startDate(LocalDate.parse(eventData.getStartDate(), formatter)) + .endDate(LocalDate.parse(eventData.getEndDate(), formatter)) + .member(admin) + .createdTime(LocalDateTime.now()) + .updatedTime(LocalDateTime.now()) + .seq(eventData.getSeq()) + .build(); + } + + public void addEventMedia(EventMedia eventMedia){ + this.eventMedias.add(eventMedia); + } + + public void update(EventUpdateDTO dto) { + update(dto, null); + } + + public void update(Location location) { + update(null, location); + } + + private void update(EventUpdateDTO dto, Location location) { + if(dto.getTitle() != null){ + this.title = dto.getTitle(); + } + if (dto.getDescription() != null){ + this.description = dto.getDescription(); + } + if (dto.getDetailLocation() != null){ + this.detailLocation = dto.getDetailLocation(); + } + if (dto.getPrice() != null){ + this.price = dto.getPrice(); + } + if (dto.getLink() != null){ + this.link = dto.getLink(); + } + if (dto.getEventType() != null){ + this.type = dto.getEventType(); + } + if (dto.getStartDate() != null){ + this.startDate = dto.getStartDate(); + } + if (dto.getEndDate() != null){ + this.endDate = dto.getEndDate(); + } + if (dto.getDetailedSchedule() != null){ + this.detailedSchedule = dto.getDetailedSchedule(); + } + if (dto.getLocationId() != null){ + this.location = location; + } + this.updatedTime = LocalDateTime.now(); + } + + public boolean equalUsername(String username) { + return this.member.getUsername().equals(username); + } + + public void setType(EventType eventType) { + this.type = eventType; + } + + public boolean isPersist() { + return this.id != null; + } + + public void updateEventIfChanged(XmlDetailExhibitionData detailEventData, Location location) { + if(!this.title.equals(detailEventData.getTitle())){ + this.title = detailEventData.getTitle(); + } + if(!this.description.equals(detailEventData.getContents1() + "\n" + detailEventData.getContents2())) { + this.description = detailEventData.getContents1() + "\n" + detailEventData.getContents2(); + } + if(!this.detailLocation.equals(detailEventData.getPlace())){ + this.detailLocation = detailEventData.getPlace(); + } + if(!this.price.equals(detailEventData.getPrice())){ + this.price = detailEventData.getPrice(); + } + if(!this.link.equals(detailEventData.getUrl())){ + this.link = detailEventData.getUrl(); + } + if(!this.startDate.equals(LocalDate.parse(detailEventData.getStartDate()))){ + this.startDate = LocalDate.parse(detailEventData.getStartDate()); + } + if(!this.endDate.equals(LocalDate.parse(detailEventData.getEndDate()))){ + this.endDate = LocalDate.parse(detailEventData.getEndDate()); + } + if(!this.location.equals(location)){ + this.location = location; + } + this.updatedTime = LocalDateTime.now(); + + } + + public void setLocation(Location location) { + this.location = location; + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java new file mode 100644 index 00000000..188082ba --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java @@ -0,0 +1,86 @@ +package com.example.codebase.domain.exhibition.entity; + +import com.example.codebase.domain.exhibition.crawling.dto.detailExhbitionResponse.XmlDetailExhibitionData; +import com.example.codebase.domain.exhibition.dto.ExhbitionCreateDTO; +import com.example.codebase.domain.exhibition.dto.ExhibitionMediaCreateDTO; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "event_media") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class EventMedia { + + @Id + @Column(name = "event_media_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "media_type", nullable = false) + private EventMediaType eventMediaType; + + @Column(name = "media_url", nullable = false) + private String mediaUrl; + + @Column(name = "created_time") + private LocalDateTime createdTime; + + @Column(name = "updated_time") + private LocalDateTime updatedTime; + + @ManyToOne + @JoinColumn(name = "event_id") + private Event event; + + public static List of(Exhibition exhibition, Event event) { + List eventMedias = new ArrayList<>(); + + for (int i = 0; i < exhibition.getExhibitionMedias().size(); i++) { + eventMedias.add(EventMedia.builder() + .eventMediaType(EventMediaType.image) + .mediaUrl(exhibition.getExhibitionMedias().get(i).getMediaUrl()) + .event(event) + .createdTime(exhibition.getCreatedTime()) + .updatedTime(exhibition.getCreatedTime()) + .build()); + } + + return eventMedias; + } + + public static EventMedia of(ExhibitionMediaCreateDTO mediaCreateDTO, Event event) { + return EventMedia.builder() + .eventMediaType(EventMediaType.create(mediaCreateDTO.getMediaType())) + .mediaUrl(mediaCreateDTO.getMediaUrl()) + .event(event) + .createdTime(LocalDateTime.now()) + .build(); + } + + public static EventMedia from(XmlDetailExhibitionData detailExhibitionData, Event event) { + return EventMedia.builder() + .eventMediaType(EventMediaType.image) + .mediaUrl(detailExhibitionData.getImgUrl()) + .event(event) + .createdTime(LocalDateTime.now()) + .updatedTime(LocalDateTime.now()) + .build(); + } + + public void update(ExhibitionMediaCreateDTO thumbnail) { + this.mediaUrl = thumbnail.getMediaUrl(); + this.eventMediaType = EventMediaType.create(thumbnail.getMediaType()); + this.updatedTime = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/entity/EventMediaType.java b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMediaType.java new file mode 100644 index 00000000..27f0553b --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMediaType.java @@ -0,0 +1,23 @@ +package com.example.codebase.domain.exhibition.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.stream.Stream; + +public enum EventMediaType { + image, + + video; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static EventMediaType create(String type) { + return Stream.of(EventMediaType.values()) + .filter(mediaType -> mediaType.name().equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("부적절한 미디어 타입입니다. 지원하는 형식 : " + + Stream.of(EventMediaType.values()) + .map(EventMediaType::name) + .reduce((a, b) -> a + ", " + b) + .get())); + } +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/entity/Exhibition.java b/src/main/java/com/example/codebase/domain/exhibition/entity/Exhibition.java index deb2b5e4..bcb65c13 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/entity/Exhibition.java +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/Exhibition.java @@ -183,5 +183,8 @@ private boolean hasChangedExhibition(XmlDetailExhibitionData perforInfo){ public boolean isPersist() { return this.id != null; } -} + public boolean getEnabled() { + return this.enabled; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java new file mode 100644 index 00000000..c8771767 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java @@ -0,0 +1,23 @@ +package com.example.codebase.domain.exhibition.repository; + +import com.example.codebase.domain.exhibition.entity.Event; +import com.example.codebase.domain.exhibition.entity.EventType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.Optional; +public interface EventRepository extends JpaRepository { + + @Query("SELECT e FROM Event e WHERE e.startDate >= :startDate AND e.endDate <= :endDate") + Page findAllBySearchCondition(LocalDate startDate, LocalDate endDate, PageRequest pageRequest); + + @Query("SELECT e FROM Event e WHERE e.startDate >= :startDate AND e.endDate <= :endDate AND e.type = :eventType") + Page findAllBySearchConditionAndEventType(LocalDate startDate, LocalDate endDate, EventType eventType, PageRequest pageRequest); + + @Query("SELECT e FROM Event e WHERE e.seq = :seq") + Optional findBySeq(Long seq); +} diff --git a/src/main/java/com/example/codebase/domain/exhibition/repository/ExhibitionRepository.java b/src/main/java/com/example/codebase/domain/exhibition/repository/ExhibitionRepository.java index 3860f942..be8e2e78 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/repository/ExhibitionRepository.java +++ b/src/main/java/com/example/codebase/domain/exhibition/repository/ExhibitionRepository.java @@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.Query; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface ExhibitionRepository extends JpaRepository { @@ -37,4 +38,8 @@ Page findExhibitionsWithEventSchedules( @Query("SELECT e FROM Exhibition e WHERE e.seq = :seq AND e.enabled = true") Optional findBySeq(Long seq); + + @Query(value = "SELECT * FROM exhibition", nativeQuery = true) + List findAllExhibitionsIgnoreEnabled(); + } diff --git a/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java b/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java new file mode 100644 index 00000000..c9161b5b --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java @@ -0,0 +1,164 @@ +package com.example.codebase.domain.exhibition.service; + +import com.example.codebase.controller.dto.PageInfo; +import com.example.codebase.domain.exhibition.dto.*; +import com.example.codebase.domain.exhibition.entity.*; +import com.example.codebase.domain.exhibition.repository.EventRepository; +import com.example.codebase.domain.exhibition.repository.ExhibitionRepository; +import com.example.codebase.domain.location.entity.Location; +import com.example.codebase.domain.location.repository.LocationRepository; +import com.example.codebase.domain.member.entity.Member; +import com.example.codebase.domain.member.exception.NotFoundMemberException; +import com.example.codebase.domain.member.repository.MemberRepository; +import com.example.codebase.exception.NotFoundException; +import com.example.codebase.util.SecurityUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class EventService { + + private final ExhibitionRepository exhibitionRepository; + + private final EventRepository eventRepository; + + private final MemberRepository memberRepository; + + private final LocationRepository locationRepository; + + + @Autowired + public EventService(ExhibitionRepository exhibitionRepository, EventRepository eventRepository, MemberRepository memberRepository, LocationRepository locationRepository) { + this.exhibitionRepository = exhibitionRepository; + this.eventRepository = eventRepository; + this.memberRepository = memberRepository; + this.locationRepository = locationRepository; + } + + @Transactional + public void moveEventSchedule() { + List exhibitions = exhibitionRepository.findAllExhibitionsIgnoreEnabled(); + + List events = transformExhibitionsToEvents(exhibitions); + + eventRepository.saveAll(events); + } + + private List transformExhibitionsToEvents(List exhibitions) { + List events = new ArrayList<>(); + + for (Exhibition exhibition : exhibitions) { + List eventMedias; + Event event = Event.from(exhibition); + + if (event == null) continue; + + eventMedias = EventMedia.of(exhibition, event); + event.setEventMedias(eventMedias); + events.add(event); + } + return events; + } + + @Transactional + public EventDetailResponseDTO createEvent(EventCreateDTO dto, Member member) { + Location location = findLocationByLocationId(dto.getLocationId()); + + Event event = Event.of(dto, member, location); + + EventMedia thumbnail = EventMedia.of(dto.getThumbnail(), event); + event.addEventMedia(thumbnail); + + for (ExhibitionMediaCreateDTO mediaCreateDTO : dto.getMedias()) { + EventMedia media = EventMedia.of(mediaCreateDTO, event); + event.addEventMedia(media); + } + + eventRepository.save(event); + + return EventDetailResponseDTO.from(event); + } + + public Member findMemberByUserName(String username) { + return memberRepository.findByUsername(username).orElseThrow(NotFoundMemberException::new); + } + + private Location findLocationByLocationId(Long locationId) { + return locationRepository.findById(locationId).orElseThrow(() -> new NotFoundException("장소를 찾을 수 없습니다.")); + } + + public EventPageInfoResponseDTO getEvents(EventSearchDTO eventSearchDTO, int page, int size, String sortDirection) { + Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), "createdTime"); + PageRequest pageRequest = PageRequest.of(page, size, sort); + + SearchEventType searchEventType = SearchEventType.create(eventSearchDTO.getEventType()); + EventType eventType = EventType.create(searchEventType.name()); + + Page events = findEventsBySearchCondition(eventSearchDTO, pageRequest, eventType); + + PageInfo pageInfo = PageInfo.of( page,size, events.getTotalPages(), events.getTotalElements()); + + List dtos = events.getContent().stream() + .map(EventResponseDTO::from) + .toList(); + + return EventPageInfoResponseDTO.of(dtos, pageInfo); + } + + private Page findEventsBySearchCondition(EventSearchDTO eventSearchDTO, PageRequest pageRequest, EventType eventType) { + if (eventType == null) { + return eventRepository.findAllBySearchCondition(eventSearchDTO.getStartDate(),eventSearchDTO.getEndDate(), pageRequest); + } + return eventRepository.findAllBySearchConditionAndEventType(eventSearchDTO.getStartDate(),eventSearchDTO.getEndDate(), eventType, pageRequest); + } + + @Transactional(readOnly = true) + public EventDetailResponseDTO getEventDetail(Long eventId) { + Event event = findEventById(eventId); + return EventDetailResponseDTO.from(event); + } + + private Event findEventById(Long eventId) { + return eventRepository.findById(eventId).orElseThrow(() -> new NotFoundException("이벤트를 찾을 수 없습니다.")); + } + + @Transactional + public EventDetailResponseDTO updateEvent(Long eventId, EventUpdateDTO dto, String username) { + Member member = memberRepository.findByUsername(username).orElseThrow(NotFoundMemberException::new); + + Event event = findEventById(eventId); + + if (!SecurityUtil.isAdmin() && !event.equalUsername(username)) { + throw new RuntimeException("해당 이벤트의 작성자가 아닙니다"); + } + + if(dto.getLocationId() != null){ + Location location = findLocationByLocationId(dto.getLocationId()); + event.update(location); + } + + event.update(dto); + + return EventDetailResponseDTO.from(event); + } + + @Transactional + public void deleteEvent(Long eventId, String username) { + Event event = findEventById(eventId); + + if (!SecurityUtil.isAdmin() && !event.equalUsername(username)) { + throw new RuntimeException("해당 이벤트의 작성자가 아닙니다"); + } + + eventRepository.delete(event); + } + + +} diff --git a/src/main/java/com/example/codebase/domain/image/service/ImageService.java b/src/main/java/com/example/codebase/domain/image/service/ImageService.java index 6d06f899..2eacb1ae 100644 --- a/src/main/java/com/example/codebase/domain/image/service/ImageService.java +++ b/src/main/java/com/example/codebase/domain/image/service/ImageService.java @@ -4,6 +4,7 @@ import com.example.codebase.domain.agora.dto.AgoraMediaCreateDTO; import com.example.codebase.domain.artwork.dto.ArtworkCreateDTO; import com.example.codebase.domain.artwork.dto.ArtworkMediaCreateDTO; +import com.example.codebase.domain.exhibition.dto.EventCreateDTO; import com.example.codebase.domain.exhibition.dto.ExhbitionCreateDTO; import com.example.codebase.domain.exhibition.dto.ExhibitionMediaCreateDTO; import com.example.codebase.domain.post.dto.PostCreateDTO; @@ -221,4 +222,36 @@ public void uploadThumbnail(AgoraCreateDTO dto, MultipartFile thumbnailFile) thr String savedUrl = s3Service.saveUploadFile(thumbnailFile); dto.getThumbnail().setMediaUrl(savedUrl); } + + public void uploadMedias(EventCreateDTO dto, List mediaFiles) + throws IOException { + if (mediaFiles.size() > Integer.valueOf(fileCount)) { + throw new RuntimeException("파일은 최대 " + fileCount + "개까지 업로드 가능합니다."); + } + + if (mediaFiles.size() == 0) { + throw new RuntimeException("파일을 업로드 해주세요."); + } + + for (int i = 0; i < dto.getMedias().size(); i++) { + ExhibitionMediaCreateDTO mediaDto = dto.getMedias().get(i); + + if (mediaDto.getMediaType().equals("url")) { + String youtubeUrl = new String(mediaFiles.get(i).getBytes(), StandardCharsets.UTF_8); + + if (!youtubeUrl.matches("^(https?\\:\\/\\/)?(www\\.)?(youtube\\.com|youtu\\.?be)\\/.+$")) { + throw new RuntimeException( + "유튜브 링크 형식이 올바르지 않습니다. ex) https://www.youtube.com/watch?v=XXXXXXXXXXX 또는 https://youtu.be/XXXXXXXXXXX"); + } + + mediaDto.setMediaUrl(youtubeUrl); + String savedUrl = s3Service.saveUploadFile(mediaFiles.get(i)); + mediaDto.setMediaUrl(savedUrl); + } else { + String savedUrl = s3Service.saveUploadFile(mediaFiles.get(i)); + mediaDto.setMediaUrl(savedUrl); + } + } + } + } diff --git a/src/main/java/com/example/codebase/domain/location/dto/LocationResponseDTO.java b/src/main/java/com/example/codebase/domain/location/dto/LocationResponseDTO.java index 3a706425..372009a3 100644 --- a/src/main/java/com/example/codebase/domain/location/dto/LocationResponseDTO.java +++ b/src/main/java/com/example/codebase/domain/location/dto/LocationResponseDTO.java @@ -43,7 +43,7 @@ public static LocationResponseDTO from(EventSchedule eventSchedule) { .build(); } - public static LocationResponseDTO of(Location location) { + public static LocationResponseDTO from(Location location) { return LocationResponseDTO.builder() .id(location.getId()) .latitude(location.getLatitude()) diff --git a/src/main/java/com/example/codebase/domain/location/entity/Location.java b/src/main/java/com/example/codebase/domain/location/entity/Location.java index e90c8aa6..b2a22023 100644 --- a/src/main/java/com/example/codebase/domain/location/entity/Location.java +++ b/src/main/java/com/example/codebase/domain/location/entity/Location.java @@ -1,6 +1,7 @@ package com.example.codebase.domain.location.entity; import com.example.codebase.domain.exhibition.crawling.dto.detailExhbitionResponse.XmlDetailExhibitionData; +import com.example.codebase.domain.exhibition.entity.Event; import com.example.codebase.domain.location.dto.LocationCreateDTO; import lombok.AllArgsConstructor; import lombok.Builder; @@ -9,6 +10,9 @@ import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + @Entity @Table(name = "location") @Getter @@ -22,11 +26,11 @@ public class Location { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "latitude", nullable = false) - private double latitude; + @Column(name = "latitude") + private Double latitude; - @Column(name = "longitude", nullable = false) - private double longitude; + @Column(name = "longitude") + private Double longitude; @Column(name = "address", nullable = false, length = 255) private String address; @@ -37,7 +41,7 @@ public class Location { @Column(name = "english_name", length = 255) private String englishName; - @Column(name = "phone_number", length = 255) + @Column(name = "phone_number", length = 150) private String phoneNumber; @Column(name = "web_site_url", length = 255) @@ -61,8 +65,8 @@ public static Location from(LocationCreateDTO dto) { public static Location from(XmlDetailExhibitionData perforInfo) { return Location.builder() - .latitude(Double.parseDouble(perforInfo.getGpsX())) - .longitude(Double.parseDouble(perforInfo.getGpsY())) + .latitude(perforInfo.getLatitude()) + .longitude(perforInfo.getLongitude()) .phoneNumber(perforInfo.getPhone()) .webSiteUrl(perforInfo.getUrl()) .address(perforInfo.getPlaceAddr()) diff --git a/src/main/java/com/example/codebase/domain/location/repository/LocationRepository.java b/src/main/java/com/example/codebase/domain/location/repository/LocationRepository.java index 2157c1de..aef3bd4e 100644 --- a/src/main/java/com/example/codebase/domain/location/repository/LocationRepository.java +++ b/src/main/java/com/example/codebase/domain/location/repository/LocationRepository.java @@ -15,10 +15,10 @@ public interface LocationRepository extends JpaRepository { + " WHERE l.address LIKE :keyword% OR l.name LIKE :keyword%") Page findByKeyword(String keyword, Pageable pageable); - @Query("SELECT l FROM Location l WHERE l.latitude = :gpsX AND l.longitude = :gpsY ") - Optional findByGpsXAndGpsY(String gpsX, String gpsY); + @Query("SELECT l FROM Location l WHERE (l.latitude = :gpsX AND l.longitude = :gpsY) OR l.address = :address ") + Optional findByGpsXAndGpsYOrAddress(String gpsX, String gpsY, String address); - @Query("SELECT l FROM Location l WHERE l.name = :name") - Optional findByName(String name); + @Query("SELECT l FROM Location l WHERE l.address = :address") + Optional findByName(String address); } diff --git a/src/main/java/com/example/codebase/domain/location/service/LocationService.java b/src/main/java/com/example/codebase/domain/location/service/LocationService.java index 689189b0..e26f46e0 100644 --- a/src/main/java/com/example/codebase/domain/location/service/LocationService.java +++ b/src/main/java/com/example/codebase/domain/location/service/LocationService.java @@ -52,17 +52,16 @@ public LocationResponseDTO createLocation(LocationCreateDTO dto, String username Location location = findSameLocation(dto); locationRepository.save(location); - return LocationResponseDTO.of(location); + return LocationResponseDTO.from(location); } private Location findSameLocation(LocationCreateDTO dto) { - return locationRepository.findByGpsXAndGpsY(String.valueOf(dto.getLatitude()),String.valueOf(dto.getLongitude())) - .orElseGet(() -> locationRepository.findByName(dto.getName()) - .orElseGet(() -> { - Location newLocation = Location.from(dto); - locationRepository.save(newLocation); - return newLocation; - })); + return locationRepository.findByGpsXAndGpsYOrAddress(String.valueOf(dto.getLatitude()), String.valueOf(dto.getLongitude()), dto.getName()) + .orElseGet(() -> { + Location newLocation = Location.from(dto); + locationRepository.save(newLocation); + return newLocation; + }); } @Transactional(readOnly = true) @@ -72,7 +71,7 @@ public LocationResponseDTO getLocation(Long locationId) { .findById(locationId) .orElseThrow(() -> new RuntimeException("존재하지 않는 장소입니다.")); - return LocationResponseDTO.of(location); + return LocationResponseDTO.from(location); // TODO : 스케쥴, 이벤트 관련 반환 로직 구현 필요 } diff --git a/src/main/java/com/example/codebase/job/JobService.java b/src/main/java/com/example/codebase/job/JobService.java index 8525033e..32b2ca03 100644 --- a/src/main/java/com/example/codebase/job/JobService.java +++ b/src/main/java/com/example/codebase/job/JobService.java @@ -5,8 +5,11 @@ import com.example.codebase.domain.exhibition.crawling.dto.exhibitionResponse.XmlExhibitionResponse; import com.example.codebase.domain.exhibition.crawling.service.DetailEventCrawlingService; import com.example.codebase.domain.exhibition.crawling.service.ExhibitionCrawlingService; +import com.example.codebase.domain.exhibition.entity.Event; import com.example.codebase.domain.exhibition.entity.Exhibition; +import com.example.codebase.domain.exhibition.repository.EventRepository; import com.example.codebase.domain.exhibition.repository.ExhibitionRepository; +import com.example.codebase.domain.exhibition.service.EventService; import com.example.codebase.domain.member.entity.Member; import com.example.codebase.domain.member.repository.MemberRepository; import lombok.extern.slf4j.Slf4j; @@ -15,6 +18,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.io.IOException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -31,14 +35,17 @@ public class JobService { private final ExhibitionRepository exhibitionRepository; + private final EventRepository eventRepository; @Autowired public JobService(MemberRepository memberRepository, - ExhibitionCrawlingService exhibitionCrawlingService, DetailEventCrawlingService detailEventCrawlingService, ExhibitionRepository exhibitionRepository) { + ExhibitionCrawlingService exhibitionCrawlingService, DetailEventCrawlingService detailEventCrawlingService, ExhibitionRepository exhibitionRepository, + EventRepository eventRepository) { this.memberRepository = memberRepository; this.exhibitionCrawlingService = exhibitionCrawlingService; this.detailEventCrawlingService = detailEventCrawlingService; this.exhibitionRepository = exhibitionRepository; + this.eventRepository = eventRepository; } @Scheduled(cron = "0 0/30 * * * *") // 매 30분마다 삭제 @@ -56,9 +63,8 @@ public void deleteNoneActivatedMembers() { } } - @Scheduled(cron = "0 3 * * * WED") @Transactional - public void getExhibitionListScheduler() { + public void getExhibitionListScheduler() throws IOException { log.info("[getExhibitionListScheduler JoB] 전시회 리스트 크롤링 시작"); LocalDateTime startTime = LocalDateTime.now(); List xmlResponses = exhibitionCrawlingService.loadXmlDatas(); @@ -85,5 +91,34 @@ private Member getAdmin() { return memberRepository.findByUsername("admin") .orElseThrow(() -> new RuntimeException("관리자 계정이 없습니다.")); } -} + @Scheduled(cron = "0 3 * * * WED") + @Transactional + public void getEventListScheduler() { + log.info("[getEventListScheduler JoB] 이벤트 리스트 크롤링 시작"); + LocalDateTime startTime = LocalDateTime.now(); + List xmlResponses = exhibitionCrawlingService.loadXmlDatas(); + Member admin = getAdmin(); + + for (XmlExhibitionResponse xmlResponse : xmlResponses) { + List events = xmlResponse.getXmlExhibitions(); + List eventEntities = new ArrayList<>(); + + for (XmlExhibitionData xmlExhibitionData : events) { + XmlDetailExhibitionResponse xmlDetailEventResponse = null; + try { + xmlDetailEventResponse = detailEventCrawlingService.loadAndParseXmlData(xmlExhibitionData); + } catch (IOException e) { + throw new RuntimeException(e); + } + Event event = detailEventCrawlingService.createEvent(xmlDetailEventResponse, admin); + eventEntities.add(event); + } + eventRepository.saveAll(eventEntities); + } + + log.info("[getEventListScheduler JoB] 전시회 리스트 크롤링 종료"); + LocalDateTime endTime = LocalDateTime.now(); + log.info("[getEventListScheduler JoB] 크롤링 소요시간: {} 초", endTime.getSecond() - startTime.getSecond()); + } +} diff --git a/src/main/java/com/example/codebase/s3/S3Service.java b/src/main/java/com/example/codebase/s3/S3Service.java index 9aa3f1e2..e38f6042 100644 --- a/src/main/java/com/example/codebase/s3/S3Service.java +++ b/src/main/java/com/example/codebase/s3/S3Service.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; @@ -79,6 +80,19 @@ public String saveUploadFile(MultipartFile multipartFile) throws IOException { return key; } + public String saveUploadFile(String key, byte[] file) throws IOException { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType("application/xml"); + objectMetadata.setContentLength(file.length); + + InputStream inputStream = new ByteArrayInputStream(file); + + PutObjectResult putObjectResult = amazonS3Client.putObject(new PutObjectRequest(bucket, key, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return key; + } + + public void deleteObject(String url) { DeleteObjectRequest deleteObjectRequest = new DeleteObjectRequest(bucket, url); amazonS3Client.deleteObject(deleteObjectRequest); diff --git a/src/main/resources/db/migration/V202312110951__create_event_table.sql b/src/main/resources/db/migration/V202312110951__create_event_table.sql new file mode 100644 index 00000000..0b0b4861 --- /dev/null +++ b/src/main/resources/db/migration/V202312110951__create_event_table.sql @@ -0,0 +1,26 @@ +create table event +( + event_id bigint auto_increment + primary key, + title VARCHAR(255) not null, + description text null, + detail_location varchar(255) null, + price varchar(255) null, + link varchar(200) null, + type varchar(100) not null, + enabled tinyint(1) default 1 not null, + created_time datetime not null, + updated_time datetime null, + member_id binary(16) not null, + seq int null, + start_date datetime not null, + end_date date not null, + detailed_schedule varchar(255) null, + location_id bigint null, + constraint event_ibfk_1 + foreign key (member_id) references member (member_id), + constraint event_ibfk_2 + foreign key (location_id) REFERENCES location (location_id), + constraint uk_event_id_start_date_time + unique (event_id, start_date) +); diff --git a/src/main/resources/db/migration/V202312111543__create_evnet_media_table.sql b/src/main/resources/db/migration/V202312111543__create_evnet_media_table.sql new file mode 100644 index 00000000..2b7a08a0 --- /dev/null +++ b/src/main/resources/db/migration/V202312111543__create_evnet_media_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE event_media +( + event_media_id BIGINT AUTO_INCREMENT PRIMARY KEY, + media_type VARCHAR(255) NOT NULL, + media_url VARCHAR(512) NOT NULL, + created_time DATETIME NOT NULL, + updated_time DATETIME NULL, + event_id BIGINT NOT NULL, + CONSTRAINT event_media_event_id_fk + FOREIGN KEY (event_id) REFERENCES event (event_id) +); diff --git a/src/main/resources/db/migration/V202312122122__modify_phone_number_latitude_longitude_column_to_location.sql b/src/main/resources/db/migration/V202312122122__modify_phone_number_latitude_longitude_column_to_location.sql new file mode 100644 index 00000000..64ceef57 --- /dev/null +++ b/src/main/resources/db/migration/V202312122122__modify_phone_number_latitude_longitude_column_to_location.sql @@ -0,0 +1,6 @@ +ALTER TABLE location + modify phone_number varchar(150) null, + modify latitude double null, + modify longitude double null; + + diff --git a/src/test/java/com/example/codebase/controller/EventControllerTest.java b/src/test/java/com/example/codebase/controller/EventControllerTest.java new file mode 100644 index 00000000..fb2b77cd --- /dev/null +++ b/src/test/java/com/example/codebase/controller/EventControllerTest.java @@ -0,0 +1,500 @@ +package com.example.codebase.controller; + +import com.example.codebase.domain.auth.WithMockCustomUser; +import com.example.codebase.domain.exhibition.dto.*; +import com.example.codebase.domain.exhibition.entity.*; +import com.example.codebase.domain.exhibition.repository.EventRepository; +import com.example.codebase.domain.location.entity.Location; +import com.example.codebase.domain.location.repository.LocationRepository; +import com.example.codebase.domain.member.entity.Authority; +import com.example.codebase.domain.member.entity.Member; +import com.example.codebase.domain.member.entity.MemberAuthority; +import com.example.codebase.domain.member.entity.RoleStatus; +import com.example.codebase.domain.member.repository.MemberAuthorityRepository; +import com.example.codebase.domain.member.repository.MemberRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +public class EventControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private EventRepository eventRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberAuthorityRepository memberAuthorityRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private ResourceLoader resourceLoader; + + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @BeforeEach + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build(); + } + + public Member createOrLoadMember() { + return createOrLoadMember("testid", RoleStatus.CURATOR, "ROLE_CURATOR"); + } + + public Member createOrLoadMember(String username, RoleStatus roleStatus, String... authorities) { + Optional testMember = memberRepository.findByUsername(username); + if (testMember.isPresent()) { + return testMember.get(); + } + + Member dummy = + Member.builder() + .username(username) + .password(passwordEncoder.encode("1234")) + .email(username + "@test.com") + .name(username) + .activated(true) + .createdTime(LocalDateTime.now()) + .roleStatus(roleStatus) + .build(); + + for (String authority : authorities) { + MemberAuthority memberAuthority = new MemberAuthority(); + memberAuthority.setAuthority(Authority.of(authority)); + memberAuthority.setMember(dummy); + dummy.addAuthority(memberAuthority); + } + + return memberRepository.save(dummy); + } + + private byte[] createImageFile() throws IOException { + File file = + resourceLoader.getResource("classpath:test/img.jpg").getFile(); + return Files.readAllBytes(file.toPath()); + } + + public Location createMockLocation() { + Location location = + Location.builder() + .latitude(123.123) + .longitude(123.123) + .address("주소") + .name("장소 이름") + .englishName("장소 영어 이름") + .phoneNumber("010-1234-1234") + .webSiteUrl("test.com") + .snsUrl("test.com") + .build(); + return locationRepository.save(location); + } + + public EventCreateDTO mockCreateEventDTO(int idx) { + ExhibitionMediaCreateDTO thumbnailDTO = new ExhibitionMediaCreateDTO(); + thumbnailDTO.setMediaType(ExhibtionMediaType.image.name()); + thumbnailDTO.setMediaUrl("http://localhost/"); + + ExhibitionMediaCreateDTO mediaCreateDTO = new ExhibitionMediaCreateDTO(); + mediaCreateDTO.setMediaType(ExhibtionMediaType.image.name()); + mediaCreateDTO.setMediaUrl("http://localhost/"); + + EventCreateDTO dto = new EventCreateDTO(); + dto.setTitle("test" + idx); + dto.setDescription("test description" + idx); + dto.setPrice("10000원 ~ 20000원" + idx); + dto.setLink("http://localhost/"); + dto.setEventType(EventType.WORKSHOP); + dto.setStartDate(LocalDate.now().plusDays(idx)); + dto.setEndDate(LocalDate.now().plusDays(1).plusDays(idx)); + dto.setDetailedSchedule("14시 ~ 16시"); + dto.setLocationId(createMockLocation().getId()); + dto.setDetailLocation("2층"); + dto.setThumbnail(thumbnailDTO); + dto.setMedias(Collections.singletonList(mediaCreateDTO)); + + return dto; + } + + public Event createOrLoadEvent() { + return createOrLoadEvent(1, LocalDate.now()); + } + + public Event createOrLoadEvent(int idx, LocalDate startDate) { + return createOrLoadEvent(idx, startDate, startDate); + } + + @Transactional + public Event createOrLoadEvent(int idx, LocalDate startDate, LocalDate endDate) { + + Member member = createOrLoadMember(); + + EventMedia thumbnail = EventMedia.builder() + .mediaUrl("url") + .eventMediaType(EventMediaType.image) + .createdTime(LocalDateTime.now()) + .build(); + + EventMedia media = EventMedia.builder() + .mediaUrl("url") + .eventMediaType(EventMediaType.image) + .createdTime(LocalDateTime.now()) + .build(); + + Location location = createMockLocation(); + + Event event = Event.builder() + .title("test" + idx) + .description("test description" + idx) + .price("10000원 ~ 20000원" + idx) + .link("http://localhost/") + .type(EventType.WORKSHOP) + .startDate(startDate.plusDays(idx)) + .endDate(endDate.plusDays(idx)) + .detailedSchedule("14시 ~ 16시") + .detailLocation("2층") + .location(location) + .eventMedias(new ArrayList<>(List.of(thumbnail, media))) + .createdTime(LocalDateTime.now()) + .updatedTime(LocalDateTime.now()) + .member(member) + .detailLocation("2층") + .build(); + + eventRepository.save(event); + return event; + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("이벤트 등록") + @Test + public void 이벤트_생성() throws Exception { + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + + EventCreateDTO dto = mockCreateEventDTO(0); + + MockMultipartFile dtoFile = + new MockMultipartFile("dto", "", "application/json", objectMapper.writeValueAsBytes(dto)); + + MockMultipartFile mediaFile = + new MockMultipartFile("mediaFiles", "image.jpg", "image/jpg", createImageFile()); + + MockMultipartFile thumbnailFile = + new MockMultipartFile("thumbnailFile", "image.jpg", "image/jpg", createImageFile()); + + mockMvc + .perform( + multipart("/api/events") + .file(dtoFile) + .file(mediaFile) + .file(thumbnailFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8")) + .andDo(print()) + .andExpect(status().isCreated()); + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("일정이 없는 이벤트 등록시") + @Test + public void 일정이_없는_이벤트_등록() throws Exception { + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + + EventCreateDTO dto = mockCreateEventDTO(0); + dto.setStartDate(null); + dto.setEndDate(null); + + MockMultipartFile dtoFile = + new MockMultipartFile("dto", "", "application/json", objectMapper.writeValueAsBytes(dto)); + + MockMultipartFile mediaFile = + new MockMultipartFile("mediaFiles", "image.jpg", "image/jpg", createImageFile()); + + MockMultipartFile thumbnailFile = + new MockMultipartFile("thumbnailFile", "image.jpg", "image/jpg", createImageFile()); + + mockMvc + .perform( + multipart("/api/events") + .file(dtoFile) + .file(mediaFile) + .file(thumbnailFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("시작과 종료 날이 같은 이벤트 등록시") + @Test + public void 시작과_종료_날이_같은_이벤트_등록시() throws Exception { + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + + EventCreateDTO dto = mockCreateEventDTO(0); + dto.setEndDate(dto.getStartDate()); + + MockMultipartFile dtoFile = + new MockMultipartFile("dto", "", "application/json", objectMapper.writeValueAsBytes(dto)); + + MockMultipartFile mediaFile = + new MockMultipartFile("mediaFiles", "image.jpg", "image/jpg", createImageFile()); + + MockMultipartFile thumbnailFile = + new MockMultipartFile("thumbnailFile", "image.jpg", "image/jpg", createImageFile()); + + mockMvc + .perform( + multipart("/api/events") + .file(dtoFile) + .file(mediaFile) + .file(thumbnailFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8")) + .andDo(print()) + .andExpect(status().isCreated()); + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("권한 없는 유저가 이벤트를 등록할시") + @Test + public void 권한이_없는_유저가_이벤트를_등록할시() throws Exception{ + createOrLoadMember("user", RoleStatus.NONE, "ROLE_USER"); + + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + + EventCreateDTO dto = mockCreateEventDTO(0); + dto.setEndDate(dto.getStartDate()); + + MockMultipartFile dtoFile = + new MockMultipartFile("dto", "", "application/json", objectMapper.writeValueAsBytes(dto)); + + MockMultipartFile mediaFile = + new MockMultipartFile("mediaFiles", "image.jpg", "image/jpg", createImageFile()); + + MockMultipartFile thumbnailFile = + new MockMultipartFile("thumbnailFile", "image.jpg", "image/jpg", createImageFile()); + + mockMvc + .perform( + multipart("/api/events") + .file(dtoFile) + .file(mediaFile) + .file(thumbnailFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("종료일이 시작일 보다 빠른 이벤트 등록시") + @Test + public void 종료일이_시작일_보다_빠른_이벤트_등록시() throws Exception { + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + + EventCreateDTO dto = mockCreateEventDTO(0); + dto.setEndDate(dto.getStartDate().minusDays(1)); + + MockMultipartFile dtoFile = + new MockMultipartFile("dto", "", "application/json", objectMapper.writeValueAsBytes(dto)); + + MockMultipartFile mediaFile = + new MockMultipartFile("mediaFiles", "image.jpg", "image/jpg", createImageFile()); + + MockMultipartFile thumbnailFile = + new MockMultipartFile("thumbnailFile", "image.jpg", "image/jpg", createImageFile()); + + mockMvc + .perform( + multipart("/api/events") + .file(dtoFile) + .file(mediaFile) + .file(thumbnailFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("이벤트 목록 조회 - 성공") + @Test + public void 이벤트를_전체_조회합니다() throws Exception { + createOrLoadEvent(1, LocalDate.now()); + createOrLoadEvent(2, LocalDate.now().plusWeeks(1)); + createOrLoadEvent(3, LocalDate.now().plusDays(1)); + createOrLoadEvent(4, LocalDate.now().minusDays(1)); + createOrLoadEvent(5, LocalDate.now().minusWeeks(1)); + createOrLoadEvent(6, LocalDate.now().minusMonths(1)); + + EventSearchDTO eventSearchDTO = new EventSearchDTO(); + eventSearchDTO.setEventType("ALL"); + + int page = 0; + int size = 10; + String sortDirection = "DESC"; + + mockMvc + .perform( + get("/api/events") + .param("startDate", eventSearchDTO.getStartDate().toString()) + .param("endDate", eventSearchDTO.getEndDate().toString()) + .param("eventType", "ALL") + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sortDirection", sortDirection)) + .andDo(print()) + .andExpect(status().isOk()); + } + + @DisplayName("이벤트 목록 시작일 종료일 지정 조회 - 성공") + @Test + public void 이벤트_특정_시간_사이를_조회합니다() throws Exception { + createOrLoadEvent(1, LocalDate.now()); + createOrLoadEvent(2, LocalDate.now().plusWeeks(1)); + createOrLoadEvent(3, LocalDate.now().plusDays(1)); + createOrLoadEvent(4, LocalDate.now().minusDays(1)); + createOrLoadEvent(5, LocalDate.now().minusWeeks(1)); + createOrLoadEvent(6, LocalDate.now().minusMonths(1)); + + EventSearchDTO eventSearchDTO = new EventSearchDTO(); + eventSearchDTO.setStartDate(LocalDate.now().minusDays(2)); + eventSearchDTO.setEndDate(LocalDate.now().plusDays(2)); + eventSearchDTO.setEventType("ALL"); + + int page = 0; + int size = 10; + String sortDirection = "DESC"; + + mockMvc + .perform( + get("/api/events") + .param("startDate", eventSearchDTO.getStartDate().toString()) + .param("endDate", eventSearchDTO.getEndDate().toString()) + .param("eventType", "ALL") + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sortDirection", sortDirection)) + .andDo(print()) + .andExpect(status().isOk()); + } + + @WithMockCustomUser(username = "testid", role = "CURATOR") + @DisplayName("이벤트 수정") + @Test + public void 이벤트_수정() throws Exception { + createOrLoadMember("testid", RoleStatus.CURATOR, "ROLE_CURATOR"); + Event event = createOrLoadEvent(); + + EventUpdateDTO dto = new EventUpdateDTO(); + dto.setTitle("수정된 제목"); + dto.setDescription("수정된 설명 공지"); + + mockMvc + .perform( + put(String.format("/api/events/%d", event.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andDo(print()) + .andExpect(status().isOk()); + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("작성자가 아닌 유저가 이벤트 수정") + @Test + public void 작성자가_아닌_유저가_이벤트_수정() throws Exception { + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + Event event = createOrLoadEvent(); + + EventUpdateDTO dto = new EventUpdateDTO(); + dto.setTitle("수정된 제목"); + dto.setDescription("수정된 설명 공지"); + + mockMvc + .perform( + put(String.format("/api/events/%d", event.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @WithMockCustomUser(username = "testid", role = "CURATOR") + @DisplayName("이벤트 삭제") + @Test + public void 이벤트_삭제() throws Exception { + createOrLoadMember("testid", RoleStatus.CURATOR, "ROLE_CURATOR"); + Event event = createOrLoadEvent(); + + mockMvc + .perform(delete(String.format("/api/events/%d", event.getId()))) + .andDo(print()) + .andExpect(status().isOk()); + } + + @WithMockCustomUser(username = "user", role = "CURATOR") + @DisplayName("작성자가 아닌 유저가 이벤트 삭제") + @Test + public void 작성자가_아닌_유저가_이벤트_삭제() throws Exception { + createOrLoadMember("user", RoleStatus.CURATOR, "ROLE_CURATOR"); + Event event = createOrLoadEvent(); + + mockMvc + .perform(delete(String.format("/api/events/%d", event.getId()))) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + +}