From 203000feedbbb3d864e1091b50b7a8a8ee67f9f8 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Mon, 11 Dec 2023 17:35:55 +0900 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20event,=20evnet=5Fmedia=20table=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V202312110951__create_event_table.sql | 26 +++++++++++++++++++ ...202312111543__create_evnet_media_table.sql | 11 ++++++++ 2 files changed, 37 insertions(+) create mode 100644 src/main/resources/db/migration/V202312110951__create_event_table.sql create mode 100644 src/main/resources/db/migration/V202312111543__create_evnet_media_table.sql 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..0ea9d31b --- /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) +); From 68266eec67455ad0ff7362b64e6ba2a8a6d942a3 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Mon, 11 Dec 2023 18:05:53 +0900 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20event,=20evnet=5Fmedia=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exhibition/entity/Event.java | 135 ++++++++++++++++++ .../domain/exhibition/entity/EventMedia.java | 58 ++++++++ .../exhibition/entity/EventMediaType.java | 23 +++ .../repository/EventRepository.java | 9 ++ 4 files changed, 225 insertions(+) create mode 100644 src/main/java/com/example/codebase/domain/exhibition/entity/Event.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/entity/EventMediaType.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java 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..cc0926a1 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java @@ -0,0 +1,135 @@ +package com.example.codebase.domain.exhibition.entity; + + +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.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; + + @Column(name = "enabled", nullable = false) + private Boolean enabled; + + @Column(name = "seq") + private Long seq; + + @Column(name = "start_date", nullable = false) + private LocalDateTime startDate; //TODO : LocalDate으로 변경 + + @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) { + System.out.println("exhibition = " + exhibition.getId()); + + 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(); + + Event event = 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()) + .endDate(exhibition.getEventSchedules().get(0).getEndDateTime().toLocalDate()) + .detailedSchedule(exhibition.getEventSchedules().get(0).getDetailLocation()) + + .startDate(earliestSchedule.getStartDateTime()) + .endDate(latestSchedule.getStartDateTime().toLocalDate()) + + + .createdTime(exhibition.getCreatedTime()) + .updatedTime(exhibition.getUpdatedTime() != null ? exhibition.getUpdatedTime() : exhibition.getCreatedTime()) + + .member(exhibition.getMember()) + .location(location) + .build(); + + location.addEvent(event); + + return event; + } + + public void setEventMedias(List eventMedias) { + this.eventMedias = eventMedias; + } +} 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..8f41a67d --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java @@ -0,0 +1,58 @@ +package com.example.codebase.domain.exhibition.entity; + +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; + } +} 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/repository/EventRepository.java b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java new file mode 100644 index 00000000..734189b2 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java @@ -0,0 +1,9 @@ +package com.example.codebase.domain.exhibition.repository; + +import com.example.codebase.domain.exhibition.entity.Event; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EventRepository extends JpaRepository { +} From 222f69a2b7874124259e1ed465d5645c24a82282 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Mon, 11 Dec 2023 18:07:48 +0900 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20exhibition,=20event=5Fschedule?= =?UTF-8?q?=EB=A5=BC=20event,=20evnet=5Fmedia=EC=97=90=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ExhibitionController.java | 13 +++++ .../domain/exhibition/entity/Exhibition.java | 5 +- .../repository/ExhibitionRepository.java | 5 ++ .../exhibition/service/EventService.java | 55 +++++++++++++++++++ .../domain/location/entity/Location.java | 11 ++++ .../com/example/codebase/job/JobService.java | 14 ++++- 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/codebase/domain/exhibition/service/EventService.java diff --git a/src/main/java/com/example/codebase/controller/ExhibitionController.java b/src/main/java/com/example/codebase/controller/ExhibitionController.java index 615820ef..46236f53 100644 --- a/src/main/java/com/example/codebase/controller/ExhibitionController.java +++ b/src/main/java/com/example/codebase/controller/ExhibitionController.java @@ -148,4 +148,17 @@ public ResponseEntity crawlingExhibition() { return new ResponseEntity("이벤트가 업데이트 되었습니다.", HttpStatus.OK); } + + @Operation(summary = "이벤트 스케줄 이동 작업", description = "이벤트 스케줄을 이동합니다.") + @PreAuthorize("isAuthenticated()") + @PostMapping("/move/event-schedule") + public ResponseEntity moveEventSchedule(){ + if(!SecurityUtil.isAdmin()){ + throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); + } + jobService.moveEventSchedule(); + + return new ResponseEntity("이벤트 스케줄이 이동되었습니다.", HttpStatus.OK); + } + } 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/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..3624cdca --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java @@ -0,0 +1,55 @@ +package com.example.codebase.domain.exhibition.service; + +import com.example.codebase.domain.exhibition.entity.Event; +import com.example.codebase.domain.exhibition.entity.EventMedia; +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 org.springframework.beans.factory.annotation.Autowired; +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; + + + @Autowired + public EventService(ExhibitionRepository exhibitionRepository, EventRepository eventRepository) { + this.exhibitionRepository = exhibitionRepository; + this.eventRepository = eventRepository; + } + + @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; + } + + +} 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..b497f259 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 @@ -46,6 +50,9 @@ public class Location { @Column(name = "sns_url", length = 255) private String snsUrl; + @OneToMany(mappedBy = "location", cascade = CascadeType.ALL) + private List events = new ArrayList<>(); + public static Location from(LocationCreateDTO dto) { return Location.builder() .latitude(dto.getLatitude()) @@ -69,4 +76,8 @@ public static Location from(XmlDetailExhibitionData perforInfo) { .name(perforInfo.getPlace()) .build(); } + + public void addEvent (Event event) { + this.events.add(event); + } } diff --git a/src/main/java/com/example/codebase/job/JobService.java b/src/main/java/com/example/codebase/job/JobService.java index 8525033e..70a6771d 100644 --- a/src/main/java/com/example/codebase/job/JobService.java +++ b/src/main/java/com/example/codebase/job/JobService.java @@ -7,6 +7,7 @@ import com.example.codebase.domain.exhibition.crawling.service.ExhibitionCrawlingService; import com.example.codebase.domain.exhibition.entity.Exhibition; 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; @@ -31,14 +32,18 @@ public class JobService { private final ExhibitionRepository exhibitionRepository; + private final EventService eventService; + @Autowired public JobService(MemberRepository memberRepository, - ExhibitionCrawlingService exhibitionCrawlingService, DetailEventCrawlingService detailEventCrawlingService, ExhibitionRepository exhibitionRepository) { + ExhibitionCrawlingService exhibitionCrawlingService, DetailEventCrawlingService detailEventCrawlingService, ExhibitionRepository exhibitionRepository, + EventService eventService) { this.memberRepository = memberRepository; this.exhibitionCrawlingService = exhibitionCrawlingService; this.detailEventCrawlingService = detailEventCrawlingService; this.exhibitionRepository = exhibitionRepository; + this.eventService = eventService; } @Scheduled(cron = "0 0/30 * * * *") // 매 30분마다 삭제 @@ -85,5 +90,12 @@ private Member getAdmin() { return memberRepository.findByUsername("admin") .orElseThrow(() -> new RuntimeException("관리자 계정이 없습니다.")); } + + public void moveEventSchedule() { + log.info("[moveEventSchedule JoB] 이벤트 스케줄 이벤트와 동기화 시작"); + + eventService.moveEventSchedule(); + + } } From 2b3b18d69d133ff1363bc039e85114469f57c6ff Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:10:32 +0900 Subject: [PATCH 04/26] =?UTF-8?q?feat:=20event=20table=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exhibition/entity/Event.java | 144 ++++++++++++++++-- .../V202312110951__create_event_table.sql | 2 +- 2 files changed, 132 insertions(+), 14 deletions(-) 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 index cc0926a1..0f3fcca7 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java @@ -1,6 +1,10 @@ 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.*; @@ -12,6 +16,7 @@ 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; @@ -46,18 +51,19 @@ public class Event { private String link; @Builder.Default - @Column(name = "type", nullable = false) + @Column(name = "event_type", nullable = false) @Enumerated(EnumType.STRING) private EventType type = EventType.STANDARD; - @Column(name = "enabled", nullable = false) - private Boolean enabled; + @Builder.Default + @Column(name = "enabled") + private boolean enabled = true; @Column(name = "seq") private Long seq; @Column(name = "start_date", nullable = false) - private LocalDateTime startDate; //TODO : LocalDate으로 변경 + private LocalDate startDate; @Column(name = "end_date" , nullable = false) private LocalDate endDate; @@ -84,7 +90,6 @@ public class Event { private Location location; public static Event from(Exhibition exhibition) { - System.out.println("exhibition = " + exhibition.getId()); if(exhibition.getEventSchedules().isEmpty()){ return null; @@ -100,7 +105,7 @@ public static Event from(Exhibition exhibition) { Location location = exhibition.getEventSchedules().get(0).getLocation(); - Event event = Event.builder() + return Event.builder() .title(exhibition.getTitle()) .description(exhibition.getDescription()) .detailLocation(exhibition.getEventSchedules().get(0).getDetailLocation()) @@ -109,27 +114,140 @@ public static Event from(Exhibition exhibition) { .type(exhibition.getType()) .enabled(exhibition.getEnabled()) .seq(exhibition.getSeq()) - .startDate(exhibition.getEventSchedules().get(0).getStartDateTime()) + .startDate(exhibition.getEventSchedules().get(0).getStartDateTime().toLocalDate()) .endDate(exhibition.getEventSchedules().get(0).getEndDateTime().toLocalDate()) .detailedSchedule(exhibition.getEventSchedules().get(0).getDetailLocation()) - .startDate(earliestSchedule.getStartDateTime()) + .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(); - - location.addEvent(event); - - return event; } 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, 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/resources/db/migration/V202312110951__create_event_table.sql b/src/main/resources/db/migration/V202312110951__create_event_table.sql index 0ea9d31b..0b0b4861 100644 --- a/src/main/resources/db/migration/V202312110951__create_event_table.sql +++ b/src/main/resources/db/migration/V202312110951__create_event_table.sql @@ -7,7 +7,7 @@ create table event detail_location varchar(255) null, price varchar(255) null, link varchar(200) null, - type varchar(100) not null, + type varchar(100) not null, enabled tinyint(1) default 1 not null, created_time datetime not null, updated_time datetime null, From 5fc56c338ad6ce7160130bd87356ba3667dc2879 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:13:06 +0900 Subject: [PATCH 05/26] =?UTF-8?q?refector:=20location=20phone=5Fnumber=20s?= =?UTF-8?q?ize=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20latitude,longitude=20nul?= =?UTF-8?q?leble=20=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...y_phone_number_latitude_longitude_column_to_location.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/resources/db/migration/V202312122122__modify_phone_number_latitude_longitude_column_to_location.sql 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; + + From eb1bc7da8f769c6f62e110a79db81f77db73c4b2 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:15:26 +0900 Subject: [PATCH 06/26] =?UTF-8?q?refector:=20location=20entity=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/location/entity/Location.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) 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 b497f259..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 @@ -26,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; @@ -41,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) @@ -50,9 +50,6 @@ public class Location { @Column(name = "sns_url", length = 255) private String snsUrl; - @OneToMany(mappedBy = "location", cascade = CascadeType.ALL) - private List events = new ArrayList<>(); - public static Location from(LocationCreateDTO dto) { return Location.builder() .latitude(dto.getLatitude()) @@ -68,16 +65,12 @@ 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()) .name(perforInfo.getPlace()) .build(); } - - public void addEvent (Event event) { - this.events.add(event); - } } From 1cf58407236b0c891b1ebbda5e3f3251a1a13ad8 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:16:17 +0900 Subject: [PATCH 07/26] =?UTF-8?q?fix:=20location=EC=9D=84=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=EB=A1=9C=20=EC=B0=BE=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?name=20->=20address=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/location/repository/LocationRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..b606eb4d 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 @@ -18,7 +18,7 @@ public interface LocationRepository extends JpaRepository { @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.name = :name") - Optional findByName(String name); + @Query("SELECT l FROM Location l WHERE l.address = :address") + Optional findByName(String address); } From 313cdda7152e7b61de7075e15f6e51fa382a8f9c Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:20:24 +0900 Subject: [PATCH 08/26] =?UTF-8?q?feat:=20event=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codebase/controller/EventController.java | 118 ++++++++++++++++ .../domain/exhibition/dto/EventCreateDTO.java | 68 ++++++++++ .../dto/EventDetailResponseDTO.java | 93 +++++++++++++ .../exhibition/dto/EventResponseDTO.java | 61 +++++++++ .../domain/exhibition/dto/EventSearchDTO.java | 28 ++++ .../domain/exhibition/dto/EventUpdateDTO.java | 54 ++++++++ .../repository/EventRepository.java | 16 +++ .../exhibition/service/EventService.java | 127 +++++++++++++++++- 8 files changed, 560 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/codebase/controller/EventController.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/dto/EventDetailResponseDTO.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/dto/EventResponseDTO.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/dto/EventSearchDTO.java create mode 100644 src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java 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..40582b57 --- /dev/null +++ b/src/main/java/com/example/codebase/controller/EventController.java @@ -0,0 +1,118 @@ +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.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.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Tag(name = "Event", description = "이벤트 API") +@RestController +@RequestMapping("/api/events") +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("로그인이 필요합니다.")); + + imageService.uploadMedias(dto, mediaFiles); + imageService.uploadThumbnail(dto.getThumbnail(), thumbnailFile); + + EventDetailResponseDTO event= eventService.createEvent(dto, username); + + 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()") + @PostMapping("/crawling/event") + public ResponseEntity crawlingEvent() { + + if (!SecurityUtil.isAdmin()) { + throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); + } + jobService.getEventListScheduler(); + + return new ResponseEntity("이벤트가 업데이트 되었습니다.", HttpStatus.OK); + } + +} 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..da0ec149 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java @@ -0,0 +1,68 @@ +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 = "시작일은 필수입니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @NotNull(message = "종료일은 필수입니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @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/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/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..da6fd7f1 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java @@ -0,0 +1,54 @@ +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) + @DateTimeFormat(pattern = "yyyy-MM-dd") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @Parameter(required = false) + @DateTimeFormat(pattern = "yyyy-MM-dd") + @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/repository/EventRepository.java b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java index 734189b2..25b7e224 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java +++ b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java @@ -1,9 +1,25 @@ 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; + @Repository 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/service/EventService.java b/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java index 3624cdca..604a6ec9 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java +++ b/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java @@ -1,11 +1,21 @@ package com.example.codebase.domain.exhibition.service; -import com.example.codebase.domain.exhibition.entity.Event; -import com.example.codebase.domain.exhibition.entity.EventMedia; -import com.example.codebase.domain.exhibition.entity.Exhibition; +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; @@ -19,11 +29,17 @@ public class EventService { private final EventRepository eventRepository; + private final MemberRepository memberRepository; + + private final LocationRepository locationRepository; + @Autowired - public EventService(ExhibitionRepository exhibitionRepository, EventRepository eventRepository) { + public EventService(ExhibitionRepository exhibitionRepository, EventRepository eventRepository, MemberRepository memberRepository, LocationRepository locationRepository) { this.exhibitionRepository = exhibitionRepository; this.eventRepository = eventRepository; + this.memberRepository = memberRepository; + this.locationRepository = locationRepository; } @Transactional @@ -42,7 +58,7 @@ private List transformExhibitionsToEvents(List exhibitions) { List eventMedias; Event event = Event.from(exhibition); - if(event == null) continue; + if (event == null) continue; eventMedias = EventMedia.of(exhibition, event); event.setEventMedias(eventMedias); @@ -51,5 +67,106 @@ private List transformExhibitionsToEvents(List exhibitions) { return events; } + @Transactional + public EventDetailResponseDTO createEvent(EventCreateDTO dto, String username) { + dto.validateDates(); + + Member member = findMemberByUserName(username); + + if (!member.isSubmitedRoleInformation()) { + throw new RuntimeException("추가정보 입력한 사용자만 이벤트를 생성할 수 있습니다."); + } + + 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); + } + + private 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 = findMemberByUserName(username); + + Event event = findEventById(eventId); + + Location location = null; + if(dto.getLocationId() != null){ + location = findLocationByLocationId(dto.getLocationId()); + } + + if (!SecurityUtil.isAdmin() && !event.equalUsername(username)) { + throw new RuntimeException("해당 이벤트의 작성자가 아닙니다"); + } + + event.update(dto, location); + + 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); + } + } From dc11d536de215089f3ae8f3157ed2e9ddd2ff938 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:21:50 +0900 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20event=5Fmedia=EC=99=80=20event?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=EB=93=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/EventPageInfoResponseDTO.java | 24 ++++++++++++++ .../domain/exhibition/entity/EventMedia.java | 28 ++++++++++++++++ .../domain/image/service/ImageService.java | 33 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/main/java/com/example/codebase/domain/exhibition/dto/EventPageInfoResponseDTO.java 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..69988a48 --- /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 = new ArrayList<>(); + + PageInfo pageInfo = new 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/entity/EventMedia.java b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java index 8f41a67d..188082ba 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/EventMedia.java @@ -1,5 +1,8 @@ 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; @@ -55,4 +58,29 @@ public static List of(Exhibition exhibition, Event event) { 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/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); + } + } + } + } From abd63a0a914d46082142d850b06480435a40713c Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:22:31 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refector:=20location=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=A0=EB=95=8C=20of=20->=20from=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codebase/domain/location/dto/LocationResponseDTO.java | 2 +- .../codebase/domain/location/service/LocationService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/service/LocationService.java b/src/main/java/com/example/codebase/domain/location/service/LocationService.java index 689189b0..3a8faf6d 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,7 +52,7 @@ 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) { @@ -72,7 +72,7 @@ public LocationResponseDTO getLocation(Long locationId) { .findById(locationId) .orElseThrow(() -> new RuntimeException("존재하지 않는 장소입니다.")); - return LocationResponseDTO.of(location); + return LocationResponseDTO.from(location); // TODO : 스케쥴, 이벤트 관련 반환 로직 구현 필요 } From 7b19b62f53c010fb51ad0c5a2ae577d71aa72a2c Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:23:08 +0900 Subject: [PATCH 11/26] =?UTF-8?q?feat:=20event=20=EC=9C=84=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EA=B8=B0=EB=B3=B8=EA=B0=92=200.0=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XmlDetailExhibitionData.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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..1037f8da 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; - private String gpsX; - private String gpsY; + + @XmlElement(defaultValue = "0.0") + private String gpsX= String.valueOf(0.0); + + @XmlElement(defaultValue = "0.0") + private String gpsY= String.valueOf(0.0); + 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; + } + } } From 4d10aaad5decbec7a7afadbaca303165c49205d9 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:24:05 +0900 Subject: [PATCH 12/26] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20exhibiton=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/codebase/controller/ExhibitionController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/codebase/controller/ExhibitionController.java b/src/main/java/com/example/codebase/controller/ExhibitionController.java index 46236f53..03f0e848 100644 --- a/src/main/java/com/example/codebase/controller/ExhibitionController.java +++ b/src/main/java/com/example/codebase/controller/ExhibitionController.java @@ -18,6 +18,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.PositiveOrZero; +import java.io.IOException; import java.util.List; @Tag(name = "Exhibition", description = "전시회 API") @@ -140,7 +141,7 @@ public ResponseEntity deleteAllExhibition(@PathVariable Long exhibitionId) { @Operation(summary = "수동 이벤트 업데이트", description = "수동으로 공공데이터 포털에서 이벤트를 가져옵니다") @PreAuthorize("isAuthenticated()") @PostMapping("/crawling/exhibition") - public ResponseEntity crawlingExhibition() { + public ResponseEntity crawlingExhibition() throws IOException { if (!SecurityUtil.isAdmin()) { throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); } From 7f054fd36cedbab480f8c3bba27674d2ec6becf6 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:26:11 +0900 Subject: [PATCH 13/26] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EB=A7=81=EC=8B=9C=20s3=EC=97=90=20xml=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=95=9C=EB=92=A4=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=ED=9B=84=20=EC=A0=81=EC=9E=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DetailEventCrawlingService.java | 87 +++++++++++++++---- .../service/ExhibitionCrawlingService.java | 39 ++++++++- .../dto/ExhibitionMediaResponseDTO.java | 8 ++ .../com/example/codebase/job/JobService.java | 41 ++++++++- .../com/example/codebase/s3/S3Service.java | 14 +++ 5 files changed, 164 insertions(+), 25 deletions(-) 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..ba7c9c5a 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,34 +1,39 @@ 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.crawling.dto.exhibitionResponse.XmlExhibitionResponse; +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.Objects; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,12 +44,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 +64,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 +142,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 @@ -165,7 +216,7 @@ private EventType checkEventType(XmlDetailExhibitionData perforInfo) { private Location findOrCreateLocation(XmlDetailExhibitionData perforInfo) { return locationRepository.findByGpsXAndGpsY(perforInfo.getGpsX(), perforInfo.getGpsY()) - .orElseGet(() -> locationRepository.findByName(perforInfo.getRealmName()) + .orElseGet(() -> locationRepository.findByName(perforInfo.getPlaceAddr()) .orElseGet(() -> { Location newLocation = Location.from(perforInfo); locationRepository.save(newLocation); 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/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/job/JobService.java b/src/main/java/com/example/codebase/job/JobService.java index 70a6771d..066c7b38 100644 --- a/src/main/java/com/example/codebase/job/JobService.java +++ b/src/main/java/com/example/codebase/job/JobService.java @@ -5,7 +5,9 @@ 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; @@ -16,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; @@ -34,16 +37,18 @@ public class JobService { private final EventService eventService; + private final EventRepository eventRepository; @Autowired public JobService(MemberRepository memberRepository, ExhibitionCrawlingService exhibitionCrawlingService, DetailEventCrawlingService detailEventCrawlingService, ExhibitionRepository exhibitionRepository, - EventService eventService) { + EventService eventService, EventRepository eventRepository) { this.memberRepository = memberRepository; this.exhibitionCrawlingService = exhibitionCrawlingService; this.detailEventCrawlingService = detailEventCrawlingService; this.exhibitionRepository = exhibitionRepository; this.eventService = eventService; + this.eventRepository = eventRepository; } @Scheduled(cron = "0 0/30 * * * *") // 매 30분마다 삭제 @@ -61,9 +66,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(); @@ -97,5 +101,34 @@ public void moveEventSchedule() { eventService.moveEventSchedule(); } -} + @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); From 7db00bec405e37b34bd9bad7669dc7009ab3680e Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:28:49 +0900 Subject: [PATCH 14/26] =?UTF-8?q?feat:=20exhibition=20=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=EB=A7=81=20=EB=92=A4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=B0=98=ED=99=98=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crawling/service/DetailEventCrawlingService.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 ba7c9c5a..62bd37fb 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 @@ -6,7 +6,6 @@ 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.crawling.dto.exhibitionResponse.XmlExhibitionResponse; import com.example.codebase.domain.exhibition.entity.*; import com.example.codebase.domain.exhibition.repository.EventRepository; import com.example.codebase.domain.exhibition.repository.EventScheduleRepository; @@ -23,9 +22,7 @@ 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; @@ -34,7 +31,6 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -180,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) { From a05036b01a081921d41dee69ef0447e679fba6b6 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Wed, 13 Dec 2023 05:32:47 +0900 Subject: [PATCH 15/26] =?UTF-8?q?test:=20event=20test=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/EventControllerTest.java | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 src/test/java/com/example/codebase/controller/EventControllerTest.java 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()); + } + +} From 5b5db4469fcdbe908a7ddb16a84e1c265bc311de Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 18:05:55 +0900 Subject: [PATCH 16/26] =?UTF-8?q?fix:=20event=5Ftype=20=EB=A7=A4=ED=95=91?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(event=5Ftype->=20?= =?UTF-8?q?type)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/codebase/domain/exhibition/entity/Event.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0f3fcca7..ba46b958 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java @@ -51,7 +51,7 @@ public class Event { private String link; @Builder.Default - @Column(name = "event_type", nullable = false) + @Column(name = "type", nullable = false) @Enumerated(EnumType.STRING) private EventType type = EventType.STANDARD; From 2f919f9e8bfdea372cccd73d69fd78537f53a811 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 19:32:59 +0900 Subject: [PATCH 17/26] =?UTF-8?q?feat:=20@Validated=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=EA=B1=B4=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/codebase/controller/EventController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/codebase/controller/EventController.java b/src/main/java/com/example/codebase/controller/EventController.java index 40582b57..fecb679b 100644 --- a/src/main/java/com/example/codebase/controller/EventController.java +++ b/src/main/java/com/example/codebase/controller/EventController.java @@ -14,6 +14,7 @@ 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; @@ -22,6 +23,7 @@ @Tag(name = "Event", description = "이벤트 API") @RestController @RequestMapping("/api/events") +@Validated public class EventController { private final EventService eventService; @@ -60,7 +62,7 @@ public ResponseEntity createEvent( @GetMapping public ResponseEntity getEvent( @ModelAttribute @Valid EventSearchDTO eventSearchDTO, - @PositiveOrZero @RequestParam(value = "page", defaultValue = "0") int page, + @PositiveOrZero @RequestParam(value = "page", defaultValue = "0") @PositiveOrZero int page, @PositiveOrZero @RequestParam(value = "size", defaultValue = "10") int size, @RequestParam(defaultValue = "DESC", required = false) String sortDirection) { eventSearchDTO.repeatTimeValidity(); From e0444192b199a95563fc934e7f65c512761585c7 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:25:53 +0900 Subject: [PATCH 18/26] =?UTF-8?q?refector:=20location=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=202=EB=B2=88=EC=9D=84=201=EB=B2=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0=ED=96=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crawling/service/DetailEventCrawlingService.java | 5 ++--- .../domain/location/repository/LocationRepository.java | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) 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 62bd37fb..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 @@ -211,13 +211,12 @@ private EventType checkEventType(XmlDetailExhibitionData perforInfo) { } private Location findOrCreateLocation(XmlDetailExhibitionData perforInfo) { - return locationRepository.findByGpsXAndGpsY(perforInfo.getGpsX(), perforInfo.getGpsY()) - .orElseGet(() -> locationRepository.findByName(perforInfo.getPlaceAddr()) + 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/location/repository/LocationRepository.java b/src/main/java/com/example/codebase/domain/location/repository/LocationRepository.java index b606eb4d..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,8 +15,8 @@ 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.address = :address") Optional findByName(String address); From a6a4bf72a8eb7a7abfa9e30134a33b7639ac52a8 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:26:05 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refector:=20location=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=202=EB=B2=88=EC=9D=84=201=EB=B2=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0=ED=96=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/location/service/LocationService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 3a8faf6d..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 @@ -56,13 +56,12 @@ public LocationResponseDTO createLocation(LocationCreateDTO dto, String username } 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) From 32ef9a8deeb39484038b7cb4601c9673e073194f Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:28:12 +0900 Subject: [PATCH 20/26] =?UTF-8?q?refector:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=9E=A5=EC=86=8C=20=EC=88=98=EC=A0=95=20update()=20?= =?UTF-8?q?=ED=96=89=EC=9C=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exhibition/entity/Event.java | 10 ++++++- .../exhibition/service/EventService.java | 26 +++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) 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 index ba46b958..4efa271d 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java +++ b/src/main/java/com/example/codebase/domain/exhibition/entity/Event.java @@ -172,7 +172,15 @@ public void addEventMedia(EventMedia eventMedia){ this.eventMedias.add(eventMedia); } - public void update(EventUpdateDTO dto, Location location) { + 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(); } 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 index 604a6ec9..c9161b5b 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java +++ b/src/main/java/com/example/codebase/domain/exhibition/service/EventService.java @@ -68,15 +68,7 @@ private List transformExhibitionsToEvents(List exhibitions) { } @Transactional - public EventDetailResponseDTO createEvent(EventCreateDTO dto, String username) { - dto.validateDates(); - - Member member = findMemberByUserName(username); - - if (!member.isSubmitedRoleInformation()) { - throw new RuntimeException("추가정보 입력한 사용자만 이벤트를 생성할 수 있습니다."); - } - + public EventDetailResponseDTO createEvent(EventCreateDTO dto, Member member) { Location location = findLocationByLocationId(dto.getLocationId()); Event event = Event.of(dto, member, location); @@ -94,7 +86,7 @@ public EventDetailResponseDTO createEvent(EventCreateDTO dto, String username) { return EventDetailResponseDTO.from(event); } - private Member findMemberByUserName(String username) { + public Member findMemberByUserName(String username) { return memberRepository.findByUsername(username).orElseThrow(NotFoundMemberException::new); } @@ -139,20 +131,20 @@ private Event findEventById(Long eventId) { @Transactional public EventDetailResponseDTO updateEvent(Long eventId, EventUpdateDTO dto, String username) { - Member member = findMemberByUserName(username); + Member member = memberRepository.findByUsername(username).orElseThrow(NotFoundMemberException::new); Event event = findEventById(eventId); - Location location = null; - if(dto.getLocationId() != null){ - location = findLocationByLocationId(dto.getLocationId()); - } - if (!SecurityUtil.isAdmin() && !event.equalUsername(username)) { throw new RuntimeException("해당 이벤트의 작성자가 아닙니다"); } - event.update(dto, location); + if(dto.getLocationId() != null){ + Location location = findLocationByLocationId(dto.getLocationId()); + event.update(location); + } + + event.update(dto); return EventDetailResponseDTO.from(event); } From 3e354f8cd8b1dcde75e733e20f0f9b290d6cd3b7 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:29:16 +0900 Subject: [PATCH 21/26] =?UTF-8?q?refector:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EC=8B=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20controller=20=EC=9C=84=EC=9E=84,?= =?UTF-8?q?=20admin=20=EA=B6=8C=ED=95=9C=20=EC=B2=B4=ED=81=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codebase/controller/EventController.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/codebase/controller/EventController.java b/src/main/java/com/example/codebase/controller/EventController.java index fecb679b..3ba3f9b6 100644 --- a/src/main/java/com/example/codebase/controller/EventController.java +++ b/src/main/java/com/example/codebase/controller/EventController.java @@ -3,6 +3,8 @@ 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; @@ -50,10 +52,18 @@ public ResponseEntity createEvent( 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, username); + EventDetailResponseDTO event= eventService.createEvent(dto, member); return new ResponseEntity(event, HttpStatus.CREATED); } @@ -62,7 +72,7 @@ public ResponseEntity createEvent( @GetMapping public ResponseEntity getEvent( @ModelAttribute @Valid EventSearchDTO eventSearchDTO, - @PositiveOrZero @RequestParam(value = "page", defaultValue = "0") @PositiveOrZero int page, + @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(); @@ -105,13 +115,9 @@ public ResponseEntity deleteEvent(@PathVariable Long eventId) { } @Operation(summary = "수동 이벤트 크롤링 업데이트", description = "수동으로 공공데이터 포털에서 이벤트를 가져옵니다") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("isAuthenticated() AND hasRole('ROLE_ADMIN')") @PostMapping("/crawling/event") public ResponseEntity crawlingEvent() { - - if (!SecurityUtil.isAdmin()) { - throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); - } jobService.getEventListScheduler(); return new ResponseEntity("이벤트가 업데이트 되었습니다.", HttpStatus.OK); From c6b1b8f68897cb2dd35c6de7a275d731bdcc63c2 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:30:39 +0900 Subject: [PATCH 22/26] =?UTF-8?q?refector:=20@JsonFormat=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=EB=A7=8C=20datetime=20=EB=B3=80=ED=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @DateTimeFormat과 같은 역할을 수행 (중복 로직) --- .../example/codebase/domain/exhibition/dto/EventCreateDTO.java | 2 -- .../codebase/domain/exhibition/dto/EventScheduleCreateDTO.java | 2 -- .../domain/exhibition/dto/EventScheduleResponseDTO.java | 2 -- .../example/codebase/domain/exhibition/dto/EventUpdateDTO.java | 2 -- 4 files changed, 8 deletions(-) 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 index da0ec149..ff5acad3 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventCreateDTO.java @@ -35,12 +35,10 @@ public class EventCreateDTO { private EventType eventType; @NotNull(message = "시작일은 필수입니다.") - @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; @NotNull(message = "종료일은 필수입니다.") - @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; 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/EventUpdateDTO.java b/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java index da6fd7f1..6237f665 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventUpdateDTO.java @@ -33,12 +33,10 @@ public class EventUpdateDTO { private EventType eventType; @Parameter(required = false) - @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; @Parameter(required = false) - @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; From 6bc2882a319b6aa0ae390527fa2227b1005b3d13 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:31:26 +0900 Subject: [PATCH 23/26] =?UTF-8?q?refector:=20xml=20=EB=AA=85=EC=8B=9C?= =?UTF-8?q?=EC=A0=81=20=EC=B4=88=EA=B8=B0=ED=99=94=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=98=EA=B3=A0=20default=20value=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/detailExhbitionResponse/XmlDetailExhibitionData.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1037f8da..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 @@ -27,10 +27,10 @@ public class XmlDetailExhibitionData { private String imgUrl; @XmlElement(defaultValue = "0.0") - private String gpsX= String.valueOf(0.0); + private String gpsX; @XmlElement(defaultValue = "0.0") - private String gpsY= String.valueOf(0.0); + private String gpsY; private String placeUrl; private String placeAddr; From 268d0abeb2b74fe45dccca3ab1576cda3e8a5dea Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:31:43 +0900 Subject: [PATCH 24/26] =?UTF-8?q?refector:=20=EB=AA=85=EC=8B=9C=EC=A0=81?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exhibition/dto/EventPageInfoResponseDTO.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 69988a48..faa126d4 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/dto/EventPageInfoResponseDTO.java +++ b/src/main/java/com/example/codebase/domain/exhibition/dto/EventPageInfoResponseDTO.java @@ -10,9 +10,9 @@ @Setter public class EventPageInfoResponseDTO { - List events = new ArrayList<>(); + List events; - PageInfo pageInfo = new PageInfo(); + PageInfo pageInfo; public static EventPageInfoResponseDTO of( List dtos, PageInfo pageInfo) { From bfc1f9e88655d1dae4ef8d5fa5d48de638126197 Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:32:04 +0900 Subject: [PATCH 25/26] =?UTF-8?q?refector:=20=EC=9D=BC=EA=B4=80=EB=90=9C?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20@Repository=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codebase/domain/exhibition/repository/EventRepository.java | 2 -- 1 file changed, 2 deletions(-) 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 index 25b7e224..c8771767 100644 --- a/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java +++ b/src/main/java/com/example/codebase/domain/exhibition/repository/EventRepository.java @@ -10,8 +10,6 @@ import java.time.LocalDate; import java.util.Optional; - -@Repository public interface EventRepository extends JpaRepository { @Query("SELECT e FROM Event e WHERE e.startDate >= :startDate AND e.endDate <= :endDate") From d7c13eb82e8337f248d3615cdf9d1a7e54c3a42b Mon Sep 17 00:00:00 2001 From: gimdonghyeon Date: Thu, 14 Dec 2023 20:32:57 +0900 Subject: [PATCH 26/26] =?UTF-8?q?refector:=20JobService=EC=9D=98=20EventSe?= =?UTF-8?q?rvice=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ExhibitionController.java | 18 ++++++++---------- .../com/example/codebase/job/JobService.java | 12 +----------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/codebase/controller/ExhibitionController.java b/src/main/java/com/example/codebase/controller/ExhibitionController.java index d5af385d..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; @@ -30,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; } @@ -129,25 +133,19 @@ public ResponseEntity deleteEventSchedule( } @Operation(summary = "수동 이벤트 업데이트", description = "수동으로 공공데이터 포털에서 이벤트를 가져옵니다") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("isAuthenticated() AND hasRole('ROLE_ADMIN')") @PostMapping("/crawling/exhibition") public ResponseEntity crawlingExhibition() throws IOException { - if (!SecurityUtil.isAdmin()) { - throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); - } jobService.getExhibitionListScheduler(); return new ResponseEntity("이벤트가 업데이트 되었습니다.", HttpStatus.OK); } @Operation(summary = "이벤트 스케줄 이동 작업", description = "이벤트 스케줄을 이동합니다.") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("isAuthenticated() AND hasRole('ROLE_ADMIN')") @PostMapping("/move/event-schedule") public ResponseEntity moveEventSchedule(){ - if(!SecurityUtil.isAdmin()){ - throw new RuntimeException("관리자만 크롤링을 할 수 있습니다."); - } - jobService.moveEventSchedule(); + eventService.moveEventSchedule(); return new ResponseEntity("이벤트 스케줄이 이동되었습니다.", HttpStatus.OK); } diff --git a/src/main/java/com/example/codebase/job/JobService.java b/src/main/java/com/example/codebase/job/JobService.java index 066c7b38..32b2ca03 100644 --- a/src/main/java/com/example/codebase/job/JobService.java +++ b/src/main/java/com/example/codebase/job/JobService.java @@ -35,19 +35,16 @@ public class JobService { private final ExhibitionRepository exhibitionRepository; - private final EventService eventService; - private final EventRepository eventRepository; @Autowired public JobService(MemberRepository memberRepository, ExhibitionCrawlingService exhibitionCrawlingService, DetailEventCrawlingService detailEventCrawlingService, ExhibitionRepository exhibitionRepository, - EventService eventService, EventRepository eventRepository) { + EventRepository eventRepository) { this.memberRepository = memberRepository; this.exhibitionCrawlingService = exhibitionCrawlingService; this.detailEventCrawlingService = detailEventCrawlingService; this.exhibitionRepository = exhibitionRepository; - this.eventService = eventService; this.eventRepository = eventRepository; } @@ -95,13 +92,6 @@ private Member getAdmin() { .orElseThrow(() -> new RuntimeException("관리자 계정이 없습니다.")); } - public void moveEventSchedule() { - log.info("[moveEventSchedule JoB] 이벤트 스케줄 이벤트와 동기화 시작"); - - eventService.moveEventSchedule(); - - } - @Scheduled(cron = "0 3 * * * WED") @Transactional public void getEventListScheduler() {