diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java index 8f4336a9..945d3b81 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java @@ -67,4 +67,7 @@ Optional findLatestValidByStoreIdNative( @Param("price") String price, // CriterionType.PRICE.name() @Param("headcount") String headcount // CriterionType.HEADCOUNT.name() ); + + Optional findTopByPaperIdOrderByIdDesc(Long paperId); + } diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index 32556850..8c67b899 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -49,4 +50,16 @@ List findAllSuspendedByAdminWithNoPartner( Optional findTopPaperByStoreId(Long storeId); long countByStore_Id(Long storeId); + @Query(""" + SELECT p FROM Paper p + WHERE p.admin.id IN :adminIds + AND p.isActivated = :status + AND p.partnershipPeriodStart <= :today + AND p.partnershipPeriodEnd >= :today + """) + List findActivePapersByAdminIds(@Param("adminIds") List adminIds, + @Param("today") LocalDate today, + @Param("status") ActivationStatus status); + + List findByStoreIdAndAdminIdAndIsActivated(Long storeId, Long adminId, ActivationStatus isActivated); } diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java index d1c7517d..ff98eaa8 100644 --- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -21,6 +21,9 @@ import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import org.springframework.web.bind.annotation.*; + +import java.util.List; + @RestController @Tag(name = "유저 관련 api", description = "유저와 관련된 로직을 처리하는 api") @RequiredArgsConstructor @@ -73,9 +76,6 @@ public ResponseEntity>> get studentService.getUnreviewedUsage(pd.getId(), pageable))); } - - - @Operation( summary = "사용자 stamp 개수 조회 API", description = "# [v1.0 (2025-09-09)](https://www.notion.so/2691197c19ed805c980dd546adee9301?source=copy_link)\n" + @@ -90,4 +90,24 @@ public BaseResponse getStamp( ) { return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp(pd.getId())); } + + @Operation( + summary = "사용자의 이용 가능한 제휴 조회 API", + description = "# [v1.0 (2025-10-30)](https://clumsy-seeder-416.notion.site/API-29c1197c19ed8030b1f5e2a744416651?source=copy_link)\n" + + "- all = true면 전체 조회, false면 2개만 조회" + ) + @GetMapping("/usable") + public BaseResponse> getUsablePartnership( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestParam(name = "all", defaultValue = "false") boolean all + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getUsablePartnership(pd.getId(), all)); + } + + @PostMapping("/sync/all") + public BaseResponse syncAllStudentsNow() { + studentService.syncUserPapersForAllStudents(); + return BaseResponse.onSuccess(SuccessStatus._OK, "전체 학생 user_paper 동기화 완료"); + } + } diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index 92231a0d..38cf04be 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -4,6 +4,8 @@ import java.time.LocalDateTime; import java.util.List; +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -66,4 +68,20 @@ public static class CheckStampResponseDTO { private String message; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class UsablePartnershipDTO { + private Long partnershipId; + private String adminName; + private String partnerName; + private CriterionType criterionType; + private OptionType optionType; + private Integer people; + private Long cost; + private String category; + private Long discountRate; + } + } diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index c9128d11..625d4fbb 100644 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -11,4 +11,5 @@ public interface StudentRepository extends JpaRepository { Optional findStudentById(Long id); + } diff --git a/src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java b/src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java new file mode 100644 index 00000000..511998f1 --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java @@ -0,0 +1,29 @@ +package com.assu.server.domain.user.repository; + +import com.assu.server.domain.user.entity.UserPaper; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.List; + +public interface UserPaperRepository extends JpaRepository { + + @Query(""" + SELECT up FROM UserPaper up + JOIN FETCH up.paper p + LEFT JOIN FETCH p.store s + LEFT JOIN FETCH p.admin a + WHERE up.student.id = :studentId + AND p.isActivated = com.assu.server.domain.common.enums.ActivationStatus.ACTIVE + AND p.partnershipPeriodStart <= :today + AND p.partnershipPeriodEnd >= :today + ORDER BY p.id DESC + """) + List findActivePartnershipsByStudentId(@Param("studentId") Long studentId, + @Param("today") LocalDate today); + + boolean existsByStudentIdAndPaperId(Long studentId, Long paperId); + +} diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java index 595613f0..5076c46e 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentService.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java @@ -5,9 +5,12 @@ import com.assu.server.domain.user.dto.StudentResponseDTO; +import java.util.List; + public interface StudentService { StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month); StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId);//조회 - Page getUnreviewedUsage(Long memberId, Pageable pageable); + List getUsablePartnership(Long memberId, Boolean all); + void syncUserPapersForAllStudents(); } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index 64f7b5e1..d41dedcb 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -1,18 +1,30 @@ package com.assu.server.domain.user.service; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.Paper; import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.entity.enums.OptionType; +import com.assu.server.domain.partnership.repository.GoodsRepository; import com.assu.server.domain.partnership.repository.PaperContentRepository; +import com.assu.server.domain.partnership.repository.PaperRepository; import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.user.converter.StudentConverter; import com.assu.server.domain.user.dto.StudentResponseDTO; import com.assu.server.domain.user.entity.PartnershipUsage; import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.entity.UserPaper; import com.assu.server.domain.user.repository.PartnershipUsageRepository; import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.domain.user.repository.UserPaperRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import jakarta.transaction.Transactional; @@ -27,6 +39,13 @@ @RequiredArgsConstructor public class StudentServiceImpl implements StudentService { private final StudentRepository studentRepository; + private final UserPaperRepository userPaperRepository; + private final PaperContentRepository paperContentRepository; + private final PartnershipUsageRepository partnershipUsageRepository; + private final GoodsRepository goodsRepository; + private final AdminRepository adminRepository; + private final PaperRepository paperRepository; + @Override @Transactional public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) { @@ -36,9 +55,6 @@ public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) { return StudentConverter.checkStampResponseDTO(student, "스탬프 조회 성공"); } - private final PaperContentRepository paperContentRepository; - private final PartnershipUsageRepository partnershipUsageRepository; - @Override @Transactional public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) { @@ -112,4 +128,105 @@ public Page getUnreviewedUsage(Long memberId, }); } + @Override + public List getUsablePartnership(Long memberId, Boolean all) { + LocalDate today = LocalDate.now(); + + List userPapers = userPaperRepository.findActivePartnershipsByStudentId(memberId, today); + + List result = userPapers.stream().map(up -> { + Paper paper = up.getPaper(); + PaperContent content = up.getPaperContent(); + Store store = paper.getStore(); + + String adminName = (paper.getAdmin() != null) ? paper.getAdmin().getName() : null; + String partnerName = (store != null) ? store.getName() : null; + + // 카테고리 결정 로직 그대로 + String finalCategory = null; + if (content != null) { + if (content.getCategory() != null) { + finalCategory = content.getCategory(); + } else if (content.getOptionType() == OptionType.SERVICE) { + List goods = goodsRepository.findByContentId(content.getId()); + if (!goods.isEmpty()) { + finalCategory = goods.get(0).getBelonging(); + } + } + } + + return StudentResponseDTO.UsablePartnershipDTO.builder() + .partnershipId(paper.getId()) + .adminName(adminName) + .partnerName(partnerName) + .criterionType(content != null ? content.getCriterionType() : null) + .optionType(content != null ? content.getOptionType() : null) + .people(content != null ? content.getPeople() : null) + .cost(content != null ? content.getCost() : null) + .category(finalCategory) + .discountRate(content != null ? content.getDiscount() : null) + .build(); + }).toList(); + + return Boolean.FALSE.equals(all) ? result.stream().limit(2).toList() : result; + } + + @Transactional + public void syncUserPapersForStudent(Long studentId) { + Student student = studentRepository.findById(studentId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT)); + + // 1. 학생 기준으로 admin 찾기 + List admins = adminRepository.findMatchingAdmins( + student.getUniversity(), + student.getDepartment(), + student.getMajor() + ); + + if (admins.isEmpty()) { + return; + } + + List adminIds = admins.stream().map(Admin::getId).toList(); + LocalDate today = LocalDate.now(); + + // 2. admin들이 만든 오늘 유효한 paper 조회 + List papers = paperRepository.findActivePapersByAdminIds( + adminIds, + today, + ActivationStatus.ACTIVE + ); + + // 3. user_paper에 없으면 넣기 + for (Paper paper : papers) { + boolean exists = userPaperRepository.existsByStudentIdAndPaperId(studentId, paper.getId()); + if (exists) continue; + + PaperContent latestContent = paperContentRepository + .findTopByPaperIdOrderByIdDesc(paper.getId()) + .orElse(null); + + UserPaper up = UserPaper.builder() + .paper(paper) + .paperContent(latestContent) + .student(student) + .build(); + + userPaperRepository.save(up); + } + } + + /** + * 전체 학생에 대해 일괄로 user_paper 채워 넣는 메서드 + * (스케줄러에서 이거만 호출하면 됨) + */ + @Transactional + @Override + public void syncUserPapersForAllStudents() { + List students = studentRepository.findAll(); + for (Student s : students) { + syncUserPapersForStudent(s.getId()); + } + } } + diff --git a/src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java b/src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java new file mode 100644 index 00000000..70b70cbd --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserPaperScheduler { + + private final StudentServiceImpl studentService; // 또는 StudentService + + /** + * 매일 새벽 3시에 전체 학생의 user_paper를 동기화 + * cron 형식: 초 분 시 일 월 요일 + * "0 0 3 * * *" → 매일 03:00:00 + */ + @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") + public void syncAllStudentsDaily() { + studentService.syncUserPapersForAllStudents(); + } + +}