diff --git a/src/main/java/devkor/com/teamcback/domain/koyeon/controller/KoyeonController.java b/src/main/java/devkor/com/teamcback/domain/koyeon/controller/KoyeonController.java index 9314072b..dc0a1912 100644 --- a/src/main/java/devkor/com/teamcback/domain/koyeon/controller/KoyeonController.java +++ b/src/main/java/devkor/com/teamcback/domain/koyeon/controller/KoyeonController.java @@ -23,6 +23,7 @@ public class KoyeonController { /*** * 고연전 여부 반환 + * TODO: 프론트 api 변경 후 삭제 필요 */ @GetMapping("") @Operation(summary = "고연전 시즌 여부를 t/f로 반환", description = "고연전 시즌 여부를 t/f로 반환") diff --git a/src/main/java/devkor/com/teamcback/domain/koyeon/entity/Koyeon.java b/src/main/java/devkor/com/teamcback/domain/koyeon/entity/Koyeon.java index 58d8da55..685e32e5 100644 --- a/src/main/java/devkor/com/teamcback/domain/koyeon/entity/Koyeon.java +++ b/src/main/java/devkor/com/teamcback/domain/koyeon/entity/Koyeon.java @@ -3,14 +3,24 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @Table(name = "tb_koyeon") +@NoArgsConstructor public class Koyeon { @Id @Schema(description = "id", example = "1") private Long id; + @Schema(description = "name", example = "고연전") + private String name; @Schema(description = "isKoyeon", example = "true") private Boolean isKoyeon; + + public Koyeon(Long id, String name, Boolean isKoyeon) { + this.id = id; + this.name = name; + this.isKoyeon = isKoyeon; + } } diff --git a/src/main/java/devkor/com/teamcback/domain/koyeon/service/KoyeonService.java b/src/main/java/devkor/com/teamcback/domain/koyeon/service/KoyeonService.java index 241630c2..46220a11 100644 --- a/src/main/java/devkor/com/teamcback/domain/koyeon/service/KoyeonService.java +++ b/src/main/java/devkor/com/teamcback/domain/koyeon/service/KoyeonService.java @@ -1,5 +1,7 @@ package devkor.com.teamcback.domain.koyeon.service; +import devkor.com.teamcback.domain.schoolcalendar.entity.SchoolCalendar; +import devkor.com.teamcback.domain.schoolcalendar.repository.SchoolCalendarRepository; import devkor.com.teamcback.domain.koyeon.dto.response.*; import devkor.com.teamcback.domain.koyeon.entity.*; import devkor.com.teamcback.domain.koyeon.repository.*; @@ -24,13 +26,15 @@ public class KoyeonService { private final MenuRepository menuRepository; private final TagMenuRepository tagMenuRepository; private final FreePubNicknameRepository freePubNicknameRepository; + private final SchoolCalendarRepository schoolCalendarRepository; /** * 고연전 여부 확인 */ @Transactional(readOnly = true) public Koyeon isKoyeon() { - return koyeonRepository.findById(1L).orElseThrow(() -> new GlobalException(NOT_FOUND_KOYEON)); + SchoolCalendar schoolCalendar = findSchoolCalendar(); + return new Koyeon(schoolCalendar.getId(), schoolCalendar.getName(), schoolCalendar.isActive()); } /** @@ -126,4 +130,8 @@ private static List orderSequence(Map new GlobalException(NOT_FOUND_PUB)); } + + private SchoolCalendar findSchoolCalendar() { + return schoolCalendarRepository.findById(2L).orElseThrow(() -> new GlobalException(NOT_FOUND_SCHOOL_CALENDAR)); + } } diff --git a/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java b/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java index e1b7a2f5..b5ffead9 100644 --- a/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java +++ b/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java @@ -1,5 +1,6 @@ package devkor.com.teamcback.domain.operatingtime.scheduler; +import devkor.com.teamcback.domain.schoolcalendar.service.SchoolCalendarService; import devkor.com.teamcback.domain.operatingtime.entity.DayOfWeek; import devkor.com.teamcback.domain.operatingtime.service.HolidayService; import devkor.com.teamcback.domain.operatingtime.service.OperatingService; @@ -21,16 +22,7 @@ public class OperatingScheduler { private final OperatingService operatingService; private final HolidayService holidayService; private final RedisLockUtil redisLockUtil; - - private static final int SUMMER_VACATION_START_MONTH = 6; - private static final int SUMMER_VACATION_START_DAY = 22; - private static final int SUMMER_VACATION_END_MONTH = 9; - private static final int SUMMER_VACATION_END_DAY = 1; - - private static final int WINTER_VACATION_START_MONTH = 12; - private static final int WINTER_VACATION_START_DAY = 21; - private static final int WINTER_VACATION_END_MONTH = 3; - private static final int WINTER_VACATION_END_DAY = 3; + private final SchoolCalendarService schoolCalendarService; private static DayOfWeek dayOfWeek = null; private static Boolean isHoliday = null; @@ -86,7 +78,7 @@ private void setState() { log.info("dayOfWeek: {}", dayOfWeek.toString()); isHoliday = isHoliday(now); // 공휴일 여부 log.info("isHoliday: {}", isHoliday); - isVacation = isVacation(now); // 방학 여부 + isVacation = isVacation(); // 방학 여부 log.info("isVacation: {}", isVacation); isEvenWeek = false; // 토요일 짝수 주 여부 if(dayOfWeek == DayOfWeek.SATURDAY) { // 토요일이면 몇째주 토요일인지 계산 @@ -113,25 +105,8 @@ private boolean isHoliday(LocalDate date) { return holidayService.isHoliday(date); } - private boolean isVacation(LocalDate date) { - int month = date.getMonthValue(); - int day = date.getDayOfMonth(); - - // 여름방학 기간 - if((month == SUMMER_VACATION_START_MONTH && day >= SUMMER_VACATION_START_DAY) || - (month == SUMMER_VACATION_END_MONTH && day <= SUMMER_VACATION_END_DAY) || - (month > SUMMER_VACATION_START_MONTH && month < SUMMER_VACATION_END_MONTH)) { - return true; - } - - // 겨울방학 기간 - if((month == WINTER_VACATION_START_MONTH && day >= WINTER_VACATION_START_DAY) || - (month == WINTER_VACATION_END_MONTH && day <= WINTER_VACATION_END_DAY) || - (month < WINTER_VACATION_END_MONTH)) { - return true; - } - - return false; + private boolean isVacation() { + return schoolCalendarService.isVacationTf(); } private boolean isEvenWeek(LocalDate now) { diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/controller/AdminSchoolCalendarController.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/controller/AdminSchoolCalendarController.java new file mode 100644 index 00000000..51a13b8c --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/controller/AdminSchoolCalendarController.java @@ -0,0 +1,50 @@ +package devkor.com.teamcback.domain.schoolcalendar.controller; + +import devkor.com.teamcback.domain.schoolcalendar.dto.response.UpdateSchoolCalendarRes; +import devkor.com.teamcback.domain.schoolcalendar.service.SchoolCalendarService; +import devkor.com.teamcback.global.response.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin/school-calendar") +public class AdminSchoolCalendarController { + private final SchoolCalendarService schoolCalendarService; + + /*** + * 방학 여부 수정 + */ + @PutMapping("/vacation") + @Operation(summary = "방학 여부 수정(토글)", description = "방학 여부 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse updateVacationActive() { + return CommonResponse.success(schoolCalendarService.updateVacationActive()); + } + + /*** + * 고연전 여부 수정 + */ + @PutMapping("/koyeon") + @Operation(summary = "고연전 여부 수정(토글)", description = "고연전 여부 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse updateKoyeonActive() { + return CommonResponse.success(schoolCalendarService.updateKoyeonActive()); + } + +} diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/controller/SchoolCalendarController.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/controller/SchoolCalendarController.java new file mode 100644 index 00000000..409913bd --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/controller/SchoolCalendarController.java @@ -0,0 +1,50 @@ +package devkor.com.teamcback.domain.schoolcalendar.controller; + +import devkor.com.teamcback.domain.schoolcalendar.dto.response.GetSchoolCalendarRes; +import devkor.com.teamcback.domain.schoolcalendar.service.SchoolCalendarService; +import devkor.com.teamcback.global.response.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/school-calendar") +public class SchoolCalendarController { + private final SchoolCalendarService schoolCalendarService; + + /*** + * 방학 여부 반환 + */ + @GetMapping("/vacation") + @Operation(summary = "방학 여부를 t/f로 반환", description = "방학 여부를 t/f로 반환") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse isVacation() { + return CommonResponse.success(schoolCalendarService.isVacation()); + } + + /*** + * 고연전 여부 반환 + */ + @GetMapping("/koyeon") + @Operation(summary = "고연전 여부를 t/f로 반환", description = "고연전 여부를 t/f로 반환") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + public CommonResponse isKoyeon() { + return CommonResponse.success(schoolCalendarService.isKoyeon()); + } + +} diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/dto/response/GetSchoolCalendarRes.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/dto/response/GetSchoolCalendarRes.java new file mode 100644 index 00000000..6e909f58 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/dto/response/GetSchoolCalendarRes.java @@ -0,0 +1,23 @@ +package devkor.com.teamcback.domain.schoolcalendar.dto.response; + +import devkor.com.teamcback.domain.schoolcalendar.entity.SchoolCalendar; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Schema(description = "방학 여부 응답 dto") +@Getter +public class GetSchoolCalendarRes { + + @Schema(description = "학교 일정 ID", example = "1") + private Long id; + @Schema(description = "학교 일정 이름", example = "방학") + private String name; + @Schema(description = "일정 여부", example = "true") + private boolean isActive; + + public GetSchoolCalendarRes(SchoolCalendar schoolCalendar) { + this.id = schoolCalendar.getId(); + this.name = schoolCalendar.getName(); + this.isActive = schoolCalendar.isActive(); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/dto/response/UpdateSchoolCalendarRes.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/dto/response/UpdateSchoolCalendarRes.java new file mode 100644 index 00000000..39f9494b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/dto/response/UpdateSchoolCalendarRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.schoolcalendar.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "편의시설 수정 응답 dto") +@JsonIgnoreProperties +public class UpdateSchoolCalendarRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/entity/SchoolCalendar.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/entity/SchoolCalendar.java new file mode 100644 index 00000000..9bd3cdfd --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/entity/SchoolCalendar.java @@ -0,0 +1,24 @@ +package devkor.com.teamcback.domain.schoolcalendar.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Table(name = "tb_school_calendar") +@NoArgsConstructor +public class SchoolCalendar extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Setter + @Column(nullable = false) + private boolean isActive; +} diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/repository/SchoolCalendarRepository.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/repository/SchoolCalendarRepository.java new file mode 100644 index 00000000..f0db8e82 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/repository/SchoolCalendarRepository.java @@ -0,0 +1,7 @@ +package devkor.com.teamcback.domain.schoolcalendar.repository; + +import devkor.com.teamcback.domain.schoolcalendar.entity.SchoolCalendar; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SchoolCalendarRepository extends JpaRepository { +} diff --git a/src/main/java/devkor/com/teamcback/domain/schoolcalendar/service/SchoolCalendarService.java b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/service/SchoolCalendarService.java new file mode 100644 index 00000000..87a0e04d --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/schoolcalendar/service/SchoolCalendarService.java @@ -0,0 +1,71 @@ +package devkor.com.teamcback.domain.schoolcalendar.service; + +import devkor.com.teamcback.domain.schoolcalendar.dto.response.GetSchoolCalendarRes; +import devkor.com.teamcback.domain.schoolcalendar.dto.response.UpdateSchoolCalendarRes; +import devkor.com.teamcback.domain.schoolcalendar.entity.SchoolCalendar; +import devkor.com.teamcback.domain.schoolcalendar.repository.SchoolCalendarRepository; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_SCHOOL_CALENDAR; + +@Service +@RequiredArgsConstructor +public class SchoolCalendarService { + private final SchoolCalendarRepository schoolCalendarRepository; + + /** + * 방학 여부 반환 + */ + public GetSchoolCalendarRes isVacation() { + return new GetSchoolCalendarRes(findSchoolCalendar(1L)); + } + + /** + * 고연전 여부 반환 + */ + public GetSchoolCalendarRes isKoyeon() { + return new GetSchoolCalendarRes(findSchoolCalendar(2L)); + } + + /** + * 방학 여부 수정(토글) + */ + @Transactional + public UpdateSchoolCalendarRes updateVacationActive() { + updateSchoolCalendarActive(1L); + return new UpdateSchoolCalendarRes(); + } + + /** + * 고연전 여부 수정(토글) + */ + @Transactional + public UpdateSchoolCalendarRes updateKoyeonActive() { + updateSchoolCalendarActive(2L); + return new UpdateSchoolCalendarRes(); + } + + /** + * 방학 여부 반환 + */ + public boolean isVacationTf() { + SchoolCalendar schoolCalendar = schoolCalendarRepository.findById(1L).orElseThrow(() -> new GlobalException(NOT_FOUND_SCHOOL_CALENDAR)); + return schoolCalendar.isActive(); + } + + /** + * 학교 일정 진행 여부 수정 + */ + private void updateSchoolCalendarActive(long id) { + SchoolCalendar schoolCalendar = findSchoolCalendar(id); + schoolCalendar.setActive(!schoolCalendar.isActive()); // 수정 + } + + private SchoolCalendar findSchoolCalendar(Long id) { + return schoolCalendarRepository.findById(id).orElseThrow(() -> new GlobalException(NOT_FOUND_SCHOOL_CALENDAR)); + } + +} diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingDetailRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingDetailRes.java index 300def02..ef6ce34e 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingDetailRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingDetailRes.java @@ -46,7 +46,7 @@ public class SearchBuildingDetailRes { public SearchBuildingDetailRes(List facilities, List types, Building building, String imageUrl, boolean bookmarked) { this.buildingId = building.getId(); - this.name = "고려대학교 서울캠퍼스 " + building.getName(); + this.name = "고려대학교 " + building.getName(); this.address = building.getAddress(); this.latitude = building.getNode().getLatitude(); this.longitude = building.getNode().getLongitude(); diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingRes.java index 7b724bd8..8faee75a 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchBuildingRes.java @@ -33,7 +33,7 @@ public class SearchBuildingRes { public SearchBuildingRes(Building building, String imageUrl, List placeTypes) { this.buildingId = building.getId(); - this.name = "고려대학교 서울캠퍼스 " + building.getName(); + this.name = "고려대학교 " + building.getName(); this.imageUrl = imageUrl != null ? imageUrl : building.getImageUrl(); this.detail = building.getDetail(); this.address = building.getAddress(); diff --git a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java index c59c1034..255afa6e 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java +++ b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java @@ -75,16 +75,14 @@ public class SearchService { // 통합 검색 결과에 표시하지 않을 편의시설 종류 //TODO: 자전거보관소, 벤치 디자인 요청 후 List에서 제거 - // TODO: 리필로드 구현 완료되면 REUSABLE_CUP_RETURN 제거하기 private static final List excludedTypes = Arrays.asList(PlaceType.CLASSROOM.getName(), PlaceType.TOILET.getName(), PlaceType.MEN_TOILET.getName(), PlaceType.WOMEN_TOILET.getName(), PlaceType.MEN_HANDICAPPED_TOILET.getName(), - PlaceType.WOMEN_HANDICAPPED_TOILET.getName(), PlaceType.LOCKER.getName(), PlaceType.TRASH_CAN.getName(), - PlaceType.BICYCLE_RACK.getName(), PlaceType.BENCH.getName(), PlaceType.REUSABLE_CUP_RETURN.getName()); + PlaceType.WOMEN_HANDICAPPED_TOILET.getName(), PlaceType.LOCKER.getName(), PlaceType.BICYCLE_RACK.getName(), PlaceType.BENCH.getName()); // 통합 검색 결과에서 "건물명 + 기본편의시설명"의 형태로 제공되어야 하는 편의시설 종류 private static final List outerTagTypes = Arrays.asList(PlaceType.CAFE, PlaceType.CAFETERIA, PlaceType.CONVENIENCE_STORE, - PlaceType.READING_ROOM, PlaceType.STUDY_ROOM, PlaceType.BOOK_RETURN_MACHINE, PlaceType.LOUNGE, PlaceType.WATER_PURIFIER, - PlaceType.VENDING_MACHINE, PlaceType.PRINTER, PlaceType.TUMBLER_WASHER, PlaceType.ONESTOP_AUTO_MACHINE, PlaceType.BANK, + PlaceType.READING_ROOM, PlaceType.STUDY_ROOM, PlaceType.BOOK_RETURN_MACHINE, PlaceType.LOUNGE, PlaceType.WATER_PURIFIER, PlaceType.REUSABLE_CUP_RETURN, + PlaceType.VENDING_MACHINE, PlaceType.PRINTER, PlaceType.TUMBLER_WASHER, PlaceType.ONESTOP_AUTO_MACHINE, PlaceType.BANK, PlaceType.TRASH_CAN, PlaceType.SMOKING_BOOTH, PlaceType.SHOWER_ROOM, PlaceType.GYM, PlaceType.SLEEPING_ROOM, PlaceType.HEALTH_OFFICE, PlaceType.DISABLED_PARKING); // 건물 상세 조회 : 대표 편의시설 종류 diff --git a/src/main/java/devkor/com/teamcback/domain/suggestion/service/SuggestionService.java b/src/main/java/devkor/com/teamcback/domain/suggestion/service/SuggestionService.java index 10e26768..6ac266c3 100644 --- a/src/main/java/devkor/com/teamcback/domain/suggestion/service/SuggestionService.java +++ b/src/main/java/devkor/com/teamcback/domain/suggestion/service/SuggestionService.java @@ -10,7 +10,6 @@ import devkor.com.teamcback.domain.suggestion.dto.response.GetSuggestionRes; import devkor.com.teamcback.domain.suggestion.dto.response.ModifySuggestionRes; import devkor.com.teamcback.domain.suggestion.entity.Suggestion; -import devkor.com.teamcback.domain.suggestion.entity.SuggestionImage; import devkor.com.teamcback.domain.suggestion.entity.SuggestionType; import devkor.com.teamcback.domain.suggestion.repository.SuggestionRepository; import devkor.com.teamcback.domain.user.entity.User; diff --git a/src/main/java/devkor/com/teamcback/domain/vote/controller/AdminVoteController.java b/src/main/java/devkor/com/teamcback/domain/vote/controller/AdminVoteController.java new file mode 100644 index 00000000..df0f19eb --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/controller/AdminVoteController.java @@ -0,0 +1,62 @@ +package devkor.com.teamcback.domain.vote.controller; + +import devkor.com.teamcback.domain.vote.dto.response.ChangeVoteStatusRes; +import devkor.com.teamcback.domain.vote.dto.response.GetVoteRes; +import devkor.com.teamcback.domain.vote.entity.VoteStatus; +import devkor.com.teamcback.domain.vote.service.VoteService; +import devkor.com.teamcback.global.response.CommonResponse; +import devkor.com.teamcback.global.response.CursorPageRes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin/votes") +public class AdminVoteController { + private final VoteService voteService; + + /** + * 투표 상태 변경 + */ + @Operation(summary = "투표 상태 변경", description = "투표 상태 변경") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PatchMapping("/{voteId}") + public CommonResponse changeVoteStatus( + @Parameter(description = "투표 ID", example = "1") + @PathVariable(name = "voteId") Long voteId, + @Parameter(description = "투표 상태", example = "REFLECTED") + @RequestParam(name = "status") VoteStatus status) { + return CommonResponse.success(voteService.changeVoteStatus(voteId, status)); + } + + /** + * 투표 조회 + */ + @Operation(summary = "투표 조회", description = "투표 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping() + public CommonResponse> getVoteList( + @Parameter(description = "투표 상태", example = "REFLECTED") + @RequestParam(name = "status", required = false) VoteStatus status, + @Parameter(description = "마지막 조회 투표 ID", example = "0") + @RequestParam(name = "lastVoteId", defaultValue = "0", required = false) Long lastVoteId, + @Parameter(description = "조회 size", example = "20") + @RequestParam(name = "size", defaultValue = "20", required = false) int size + ) { + return CommonResponse.success(voteService.getVoteList(status, lastVoteId, size)); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/controller/VoteController.java b/src/main/java/devkor/com/teamcback/domain/vote/controller/VoteController.java new file mode 100644 index 00000000..856b6a4b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/controller/VoteController.java @@ -0,0 +1,65 @@ +package devkor.com.teamcback.domain.vote.controller; + +import devkor.com.teamcback.domain.vote.dto.request.SaveVoteRecordReq; +import devkor.com.teamcback.domain.vote.dto.response.GetVoteRes; +import devkor.com.teamcback.domain.vote.dto.response.SaveVoteRecordRes; +import devkor.com.teamcback.domain.vote.service.VoteService; +import devkor.com.teamcback.global.response.CommonResponse; +import devkor.com.teamcback.global.security.UserDetailsImpl; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/votes") +public class VoteController { + private final VoteService voteService; + + /** + * 장소별 투표 정보 조회 + * @param placeId 장소 ID + */ + @Operation(summary = "투표 정보 조회", description = "투표 정보 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping("/topics/{voteTopicId}/places/{placeId}") + public CommonResponse getVoteByPlace( + @Parameter(description = "투표 주제 ID", example = "1") + @PathVariable(name = "voteTopicId") Long voteTopicId, + @Parameter(description = "장소 ID", example = "1") + @PathVariable(name = "placeId") Long placeId) { + return CommonResponse.success(voteService.getVoteByPlace(voteTopicId, placeId)); + } + + /** + * 투표 기록 저장 + * @param userDetail 사용자 정보 + */ + @Operation(summary = "투표 저장", description = "투표 저장") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "404", description = "Not Found", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PostMapping("") + public CommonResponse saveVoteRecord( + @Parameter(description = "사용자정보") + @AuthenticationPrincipal UserDetailsImpl userDetail, + @Parameter(description = "투표 저장") + @RequestBody SaveVoteRecordReq req + ) { + Long userId = userDetail == null ? null : userDetail.getUser().getUserId(); + return CommonResponse.success(voteService.saveVoteRecord(userId, req)); + } + +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/dto/request/SaveVoteRecordReq.java b/src/main/java/devkor/com/teamcback/domain/vote/dto/request/SaveVoteRecordReq.java new file mode 100644 index 00000000..50b4d4c8 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/dto/request/SaveVoteRecordReq.java @@ -0,0 +1,15 @@ +package devkor.com.teamcback.domain.vote.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SaveVoteRecordReq { + @Schema(description = "투표 id", example = "1") + private Long voteId; + + @Schema(description = "투표 옵션 id", example = "1") + private Long voteOptionId; +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/dto/response/ChangeVoteStatusRes.java b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/ChangeVoteStatusRes.java new file mode 100644 index 00000000..59d39c8d --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/ChangeVoteStatusRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.vote.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "투표 종료 완료") +@JsonIgnoreProperties +public class ChangeVoteStatusRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/dto/response/GetVoteOptionRes.java b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/GetVoteOptionRes.java new file mode 100644 index 00000000..91156681 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/GetVoteOptionRes.java @@ -0,0 +1,26 @@ +package devkor.com.teamcback.domain.vote.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Schema(description = "투표 항목 조회 결과") +@Getter +public class GetVoteOptionRes { + + @Schema(description = "투표 항목 ID", example = "1") + private Long voteOptionId; + + @Schema(description = "투표 항목", example = "있다") + private String optionText; + + @Schema(description = "투표수", example = "5") + private int voteCount; + + @QueryProjection + public GetVoteOptionRes(Long voteOptionId, String optionText, int voteCount) { + this.voteOptionId = voteOptionId; + this.optionText = optionText; + this.voteCount = voteCount; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/dto/response/GetVoteRes.java b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/GetVoteRes.java new file mode 100644 index 00000000..335547db --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/GetVoteRes.java @@ -0,0 +1,42 @@ +package devkor.com.teamcback.domain.vote.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +import java.util.List; + +@Schema(description = "투표 목록 조회 결과") +@Getter +public class GetVoteRes { + + @Schema(description = "투표 ID", example = "1") + private Long voteId; + + @Schema(description = "투표 상태", example = "종료") + private String status; + + @Schema(description = "투표 주제 ID", example = "1") + private Long voteTopicId; + + @Schema(description = "투표 주제", example = "콘센트 유무") + private String topic; + + @Schema(description = "장소 ID", example = "1") + private Long placeId; + + @Schema(description = "장소명", example = "101호") + private String placeName; + + @Schema(description = "투표 항목 리스트") + private List optionList; + + public GetVoteRes(Long voteId, String status, Long voteTopicId, String topic, Long placeId, String placeName, List optionList) { + this.voteId = voteId; + this.voteTopicId = voteTopicId; + this.topic = topic; + this.status = status; + this.placeId = placeId; + this.placeName = placeName; + this.optionList = optionList; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/dto/response/SaveVoteRecordRes.java b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/SaveVoteRecordRes.java new file mode 100644 index 00000000..2b08c45d --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/dto/response/SaveVoteRecordRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.vote.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "투표 내용 저장 완료") +@JsonIgnoreProperties +public class SaveVoteRecordRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/entity/Vote.java b/src/main/java/devkor/com/teamcback/domain/vote/entity/Vote.java new file mode 100644 index 00000000..c4e62dd0 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/entity/Vote.java @@ -0,0 +1,40 @@ +package devkor.com.teamcback.domain.vote.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import static devkor.com.teamcback.domain.vote.entity.VoteStatus.CLOSED; +import static devkor.com.teamcback.global.response.ResultCode.CLOSED_VOTE; + +@Entity +@Getter +@Setter +@Table(name = "tb_vote") +@NoArgsConstructor +public class Vote extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long voteTopicId; + + @Column + private Long placeId; + + @Column + @Enumerated(EnumType.STRING) + private VoteStatus status; + + public void checkStatus() { + if(this.status == CLOSED) throw new GlobalException(CLOSED_VOTE); + } + + public void changeStatus(VoteStatus status) { + this.status = status; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteOption.java b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteOption.java new file mode 100644 index 00000000..e4f5cb7c --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteOption.java @@ -0,0 +1,26 @@ +package devkor.com.teamcback.domain.vote.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "tb_vote_option") +@NoArgsConstructor +public class VoteOption extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long voteTopicId; + + @Column(nullable = false) + private String optionText; + +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteRecord.java b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteRecord.java new file mode 100644 index 00000000..21b033fa --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteRecord.java @@ -0,0 +1,33 @@ +package devkor.com.teamcback.domain.vote.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "tb_vote_record") +@NoArgsConstructor +public class VoteRecord extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long userId; + + @Column(nullable = false) + private Long voteId; + + @Column + private Long voteOptionId; + + public VoteRecord(Long userId, Long voteId, Long voteOptionId) { + this.userId = userId; + this.voteId = voteId; + this.voteOptionId = voteOptionId; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteStatus.java b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteStatus.java new file mode 100644 index 00000000..bc9b6565 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteStatus.java @@ -0,0 +1,14 @@ +package devkor.com.teamcback.domain.vote.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum VoteStatus { + OPEN("진행 중"), + CLOSED("종료"), + REFLECTED("결과 반영됨"); + + private final String name; +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteTopic.java b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteTopic.java new file mode 100644 index 00000000..1d1484a2 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/entity/VoteTopic.java @@ -0,0 +1,21 @@ +package devkor.com.teamcback.domain.vote.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "tb_vote_topic") +@NoArgsConstructor +public class VoteTopic extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String topic; +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/repository/CustomVoteRepository.java b/src/main/java/devkor/com/teamcback/domain/vote/repository/CustomVoteRepository.java new file mode 100644 index 00000000..abc6870a --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/repository/CustomVoteRepository.java @@ -0,0 +1,18 @@ +package devkor.com.teamcback.domain.vote.repository; + + +import devkor.com.teamcback.domain.vote.dto.response.GetVoteOptionRes; +import devkor.com.teamcback.domain.vote.entity.Vote; +import devkor.com.teamcback.domain.vote.entity.VoteStatus; + +import java.util.List; + +public interface CustomVoteRepository { + List getVoteOptionsByPlaceByVoteTopicIdAndPlaceId(Long voteTopicId, Long placeId); + + List getVoteByStatusWithPage(VoteStatus status, Long voteTopicId, int size); + + List getVoteOptionByVoteId(Long voteId); + + List getVoteOptionByVoteIdOrderByCount(Long voteId); +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/repository/CustomVoteRepositoryImpl.java b/src/main/java/devkor/com/teamcback/domain/vote/repository/CustomVoteRepositoryImpl.java new file mode 100644 index 00000000..ed3147e9 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/repository/CustomVoteRepositoryImpl.java @@ -0,0 +1,133 @@ +package devkor.com.teamcback.domain.vote.repository; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import static devkor.com.teamcback.domain.place.entity.QPlace.place; +import static devkor.com.teamcback.domain.vote.entity.QVoteOption.voteOption; +import static devkor.com.teamcback.domain.vote.entity.QVoteRecord.voteRecord; +import static devkor.com.teamcback.domain.vote.entity.QVote.vote; + +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.place.entity.PlaceType; +import devkor.com.teamcback.domain.vote.dto.response.GetVoteOptionRes; +import devkor.com.teamcback.domain.vote.dto.response.QGetVoteOptionRes; +import devkor.com.teamcback.domain.vote.entity.Vote; +import devkor.com.teamcback.domain.vote.entity.VoteStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class CustomVoteRepositoryImpl implements CustomVoteRepository{ + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List getVoteOptionsByPlaceByVoteTopicIdAndPlaceId(Long voteTopicId, Long placeId) { + return jpaQueryFactory + .select( + new QGetVoteOptionRes( + voteOption.id, + voteOption.optionText, + voteRecord.id.count().intValue() + ) + ) + .from(vote) + .join(voteOption) + .on(vote.voteTopicId.eq(voteOption.voteTopicId)) + .leftJoin(voteRecord) + .on(voteRecord.voteId.eq(vote.id) + .and(voteRecord.voteOptionId.eq(voteOption.id))) + .where( + vote.voteTopicId.eq(voteTopicId) + .and(vote.placeId.eq(placeId)) + ) + .groupBy(voteOption.id, voteOption.optionText) + .orderBy( + voteOption.id.asc() + ) + .fetch(); + } + + @Override + public List getVoteByStatusWithPage(VoteStatus status, Long lastVoteId, int size) { + BooleanBuilder builder = new BooleanBuilder(); + builder.and(gtVoteCursor(lastVoteId)); + + if (status != null) { + builder.and(vote.status.eq(status)); + } + + return jpaQueryFactory + .selectFrom(vote) + .where( + gtVoteCursor(lastVoteId).and(builder) + ) + .orderBy( + vote.id.asc() + ) + .limit(size) + .fetch(); + } + + @Override + public List getVoteOptionByVoteId(Long voteId) { + return jpaQueryFactory + .select( + new QGetVoteOptionRes( + voteOption.id, + voteOption.optionText, + voteRecord.id.count().intValue() + ) + ) + .from(vote) + .join(voteOption) + .on(vote.voteTopicId.eq(voteOption.voteTopicId)) + .leftJoin(voteRecord) + .on(voteRecord.voteId.eq(vote.id) + .and(voteRecord.voteOptionId.eq(voteOption.id))) + .where( + vote.id.eq(voteId) + ) + .groupBy(voteOption.id, voteOption.optionText) + .orderBy( + voteOption.id.asc() + ) + .fetch(); + } + + @Override + public List getVoteOptionByVoteIdOrderByCount(Long voteId) { + return jpaQueryFactory + .select( + new QGetVoteOptionRes( + voteOption.id, + voteOption.optionText, + voteRecord.id.count().intValue() + ) + ) + .from(vote) + .join(voteOption) + .on(vote.voteTopicId.eq(voteOption.voteTopicId)) + .leftJoin(voteRecord) + .on(voteRecord.voteId.eq(vote.id) + .and(voteRecord.voteOptionId.eq(voteOption.id))) + .where( + vote.id.eq(voteId) + ) + .groupBy(voteOption.id, voteOption.optionText) + .orderBy( + voteRecord.id.count().intValue().asc() + ) + .fetch(); + } + + private BooleanExpression gtVoteCursor(Long lastVoteId) { + if (lastVoteId== null) return null; + return vote.id.gt(lastVoteId); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteOptionRepository.java b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteOptionRepository.java new file mode 100644 index 00000000..6429357d --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteOptionRepository.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.vote.repository; + +import devkor.com.teamcback.domain.vote.entity.VoteOption; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface VoteOptionRepository extends JpaRepository, CustomVoteRepository { +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteRecordRepository.java b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteRecordRepository.java new file mode 100644 index 00000000..86ed6f8b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteRecordRepository.java @@ -0,0 +1,12 @@ +package devkor.com.teamcback.domain.vote.repository; + +import devkor.com.teamcback.domain.vote.entity.VoteRecord; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface VoteRecordRepository extends JpaRepository { + VoteRecord findByUserIdAndVoteId(Long userId, Long voteId); + + boolean existsByUserIdAndVoteId(Long userId, Long voteId); +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteRepository.java b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteRepository.java new file mode 100644 index 00000000..09e14f53 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteRepository.java @@ -0,0 +1,17 @@ +package devkor.com.teamcback.domain.vote.repository; + +import devkor.com.teamcback.domain.vote.entity.Vote; +import devkor.com.teamcback.domain.vote.entity.VoteStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface VoteRepository extends JpaRepository { + Optional findByVoteTopicIdAndPlaceId(Long voteTopicId, Long placeId); + + @Query("SELECT v FROM Vote v WHERE v.status IS NULL OR v.status = :status") + List findAllByStatus(@Param("status")VoteStatus status); +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteTopicRepository.java b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteTopicRepository.java new file mode 100644 index 00000000..aa60a2ba --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/repository/VoteTopicRepository.java @@ -0,0 +1,8 @@ +package devkor.com.teamcback.domain.vote.repository; + +import devkor.com.teamcback.domain.vote.entity.VoteTopic; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface VoteTopicRepository extends JpaRepository { +} diff --git a/src/main/java/devkor/com/teamcback/domain/vote/service/VoteService.java b/src/main/java/devkor/com/teamcback/domain/vote/service/VoteService.java new file mode 100644 index 00000000..3608b0d3 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/vote/service/VoteService.java @@ -0,0 +1,166 @@ +package devkor.com.teamcback.domain.vote.service; + +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.place.repository.PlaceRepository; +import devkor.com.teamcback.domain.vote.dto.request.SaveVoteRecordReq; +import devkor.com.teamcback.domain.vote.dto.response.ChangeVoteStatusRes; +import devkor.com.teamcback.domain.vote.dto.response.GetVoteOptionRes; +import devkor.com.teamcback.domain.vote.dto.response.GetVoteRes; +import devkor.com.teamcback.domain.vote.dto.response.SaveVoteRecordRes; +import devkor.com.teamcback.domain.vote.entity.*; +import devkor.com.teamcback.domain.vote.repository.VoteOptionRepository; +import devkor.com.teamcback.domain.vote.repository.VoteRecordRepository; +import devkor.com.teamcback.domain.vote.repository.VoteRepository; +import devkor.com.teamcback.domain.vote.repository.VoteTopicRepository; +import devkor.com.teamcback.global.annotation.UpdateScore; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.CursorPageRes; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +import static devkor.com.teamcback.domain.vote.entity.VoteStatus.CLOSED; +import static devkor.com.teamcback.global.response.ResultCode.*; + +@Service +@RequiredArgsConstructor +public class VoteService { + private final PlaceRepository placeRepository; + private final VoteTopicRepository voteTopicRepository; + private final VoteOptionRepository voteOptionRepository; + private final VoteRecordRepository voteRecordRepository; + private final VoteRepository voteRepository; + + /** + * 장소별 투표 조회 + * @param voteTopicId 투표 주제 ID + * @param placeId 장소 ID + */ + @Transactional(readOnly = true) + public GetVoteRes getVoteByPlace(Long voteTopicId, Long placeId) { + + // 투표 주제 + VoteTopic voteTopic = findVoteTopic(voteTopicId); + + // 투표 + Vote vote = findVote(voteTopicId, placeId); + + // 장소 + Place place = findPlace(placeId); + + // 투표 항목, 현황 + List voteOptionList = voteOptionRepository.getVoteOptionsByPlaceByVoteTopicIdAndPlaceId(voteTopic.getId(), placeId); + + return new GetVoteRes(vote.getId(), vote.getStatus().getName(), voteTopic.getId(), voteTopic.getTopic(), place.getId(), place.getName(), voteOptionList); + } + + + /** + * 투표 저장 + */ + @UpdateScore(addScore = 3) + @Transactional + public SaveVoteRecordRes saveVoteRecord(Long userId, SaveVoteRecordReq req) { + if(userId == null) throw new GlobalException(FORBIDDEN); + + // 투표 + Vote vote = findVote(req.getVoteId()); + + // 투표한 옵션 + VoteOption voteOption = findVoteOption(req.getVoteOptionId()); + + // 투표 주제의 옵션이 아닌 경우 + if(!Objects.equals(vote.getVoteTopicId(), voteOption.getVoteTopicId())) throw new GlobalException(INVALID_INPUT); + + // 투표 상태 확인 + vote.checkStatus(); + + // 투표 이력 확인 + VoteRecord voteRecord = voteRecordRepository.findByUserIdAndVoteId(userId, vote.getId()); + if(voteRecord == null) { // 다른 거 선택한 경우 새로 저장 + voteRecordRepository.save(new VoteRecord(userId, vote.getId(), req.getVoteOptionId())); + } + else if(!Objects.equals(voteRecord.getVoteOptionId(), voteOption.getId())){ // 다른 걸 투표한 경우 + voteRecord.setVoteOptionId(req.getVoteOptionId()); + } + else { // 같은 걸 투표한 경우 -> 토글로 취소됨 + voteRecord.setVoteOptionId(null); // 기존 투표 옵션을 null로 설정 (기록 남기기) + } + + // 투표 상태 변경 + List voteOptionList = voteOptionRepository.getVoteOptionByVoteIdOrderByCount(vote.getId()); // 투표 항목, 현황 + int min = voteOptionList.get(0).getVoteCount(); + int max = voteOptionList.get(voteOptionList.size() - 1).getVoteCount(); + if(max - min >= 30) vote.changeStatus(CLOSED); + + return new SaveVoteRecordRes(); + } + + /** + * 투표 리스트 조회 + */ + @Transactional(readOnly = true) + public CursorPageRes getVoteList(VoteStatus status, Long lastVoteId, int size) { + + List resList = new ArrayList<>(); + + List voteList = voteOptionRepository.getVoteByStatusWithPage(status, lastVoteId, size + 1); + + for(Vote vote : voteList) { + // 투표 주제 + VoteTopic voteTopic = findVoteTopic(vote.getVoteTopicId()); + + // 투표 장소 + Place place = findPlace(vote.getPlaceId()); + + // 투표 항목, 현황 + List voteOptionList = voteOptionRepository.getVoteOptionByVoteId(vote.getId()); + + resList.add(new GetVoteRes(vote.getId(), vote.getStatus().toString(), voteTopic.getId(), voteTopic.getTopic(), place.getId(), place.getName(), voteOptionList)); + } + + // CursorPageRes 응답 + boolean hasNext = voteList.size() > size; + if(hasNext) { + resList.remove(size); + } + Long lastCursorId = voteList.isEmpty() ? null : voteList.get(voteList.size() - 1).getId(); + + return new CursorPageRes<>(resList, hasNext, lastCursorId); + } + + /** + * 투표 상태 변경 + */ + public ChangeVoteStatusRes changeVoteStatus(Long voteId, VoteStatus status) { + // 투표 + Vote vote = findVote(voteId); + + // 상태 변경 + vote.changeStatus(status); + + return new ChangeVoteStatusRes(); + } + + private Vote findVote(Long voteTopicId, Long placeId) { + return voteRepository.findByVoteTopicIdAndPlaceId(voteTopicId, placeId).orElseThrow(() -> new GlobalException(NOT_FOUND_VOTE)); + } + + private Vote findVote(Long voteId) { + return voteRepository.findById(voteId).orElseThrow(() -> new GlobalException(NOT_FOUND_VOTE)); + } + + private VoteTopic findVoteTopic(Long voteTopicId) { + return voteTopicRepository.findById(voteTopicId).orElseThrow(() -> new GlobalException(NOT_FOUND_VOTE_TOPIC)); + } + + private VoteOption findVoteOption(Long voteOptionId) { + return voteOptionRepository.findById(voteOptionId).orElseThrow(() -> new GlobalException(NOT_FOUND_VOTE_OPTION)); + } + + private Place findPlace(Long placeId) { + return placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(NOT_FOUND_PLACE)); + } +} diff --git a/src/main/java/devkor/com/teamcback/global/aop/UpdateScoreAspect.java b/src/main/java/devkor/com/teamcback/global/aop/UpdateScoreAspect.java index 6acb9dfb..9eb9c9d0 100644 --- a/src/main/java/devkor/com/teamcback/global/aop/UpdateScoreAspect.java +++ b/src/main/java/devkor/com/teamcback/global/aop/UpdateScoreAspect.java @@ -5,6 +5,8 @@ import devkor.com.teamcback.domain.user.entity.Level; import devkor.com.teamcback.domain.user.entity.User; import devkor.com.teamcback.domain.user.repository.UserRepository; +import devkor.com.teamcback.domain.vote.dto.request.SaveVoteRecordReq; +import devkor.com.teamcback.domain.vote.repository.VoteRecordRepository; import devkor.com.teamcback.global.annotation.UpdateScore; import devkor.com.teamcback.global.exception.exception.GlobalException; import lombok.RequiredArgsConstructor; @@ -28,6 +30,7 @@ public class UpdateScoreAspect { private final UserRepository userRepository; private final UserBookmarkLogRepository userBookmarkLogRepository; + private final VoteRecordRepository voteRecordRepository; @Around("@annotation(updateScore)") public Object updateScore(ProceedingJoinPoint joinPoint, UpdateScore updateScore) throws Throwable { @@ -88,6 +91,12 @@ private boolean checkUpdatable(User user, Object[] args) { return false; } } + + if (arg instanceof SaveVoteRecordReq req) { + if (voteRecordRepository.existsByUserIdAndVoteId(user.getUserId(), req.getVoteId())) { + return false; + } + } } return true; } diff --git a/src/main/java/devkor/com/teamcback/global/jwt/JwtAuthorizationFilter.java b/src/main/java/devkor/com/teamcback/global/jwt/JwtAuthorizationFilter.java index c85697cc..434654cf 100644 --- a/src/main/java/devkor/com/teamcback/global/jwt/JwtAuthorizationFilter.java +++ b/src/main/java/devkor/com/teamcback/global/jwt/JwtAuthorizationFilter.java @@ -32,6 +32,7 @@ public class JwtAuthorizationFilter extends OncePerRequestFilter { private static final List whiteList = List.of( new AntPathRequestMatcher("/api/migration"), + new AntPathRequestMatcher("/api/school-calendar/**"), new AntPathRequestMatcher("/api/koyeon/**"), new AntPathRequestMatcher("/api/routes/**"), new AntPathRequestMatcher("/api/search", HttpMethod.GET.name()), diff --git a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java index 880c15ad..bfe4899f 100644 --- a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java +++ b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java @@ -73,8 +73,15 @@ public enum ResultCode { // 운영 시간 10000번대 OPER_CONDITION_HAS_NO_OPER_TIME(HttpStatus.NOT_FOUND, 10000, "운영 조건이 운영 시간을 가지고 있지 않습니다."), - // 고연전 11000번대 - NOT_FOUND_TAG(HttpStatus.NOT_FOUND, 11000, "음식 태그를 찾을 수 없습니다.") + // 행사 11000번대 + NOT_FOUND_TAG(HttpStatus.NOT_FOUND, 11000, "음식 태그를 찾을 수 없습니다."), + NOT_FOUND_SCHOOL_CALENDAR(HttpStatus.NOT_FOUND, 11001, "관련 일정을 찾을 수 없습니다."), + + // 투표 12000번대 + NOT_FOUND_VOTE(HttpStatus.NOT_FOUND, 12000, "투표를 찾을 수 없습니다."), + NOT_FOUND_VOTE_TOPIC(HttpStatus.NOT_FOUND, 12001, "투표 주제를 찾을 수 없습니다."), + NOT_FOUND_VOTE_OPTION(HttpStatus.NOT_FOUND, 12002, "투표 항목을 찾을 수 없습니다."), + CLOSED_VOTE(HttpStatus.BAD_REQUEST, 12003, "종료된 투표입니다.") ; private final HttpStatus status;