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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
import java.util.List;

public interface StudentAdminRepository extends JpaRepository<StudentAdmin, Long> {

// 총 누적 가입자 수
@Query("""
select count(sa)
from StudentAdmin sa
where sa.admin.id = :adminId
""")
Long countAllByAdminId(@Param("adminId") Long adminId);


// 기간별 가입자 수
@Query("""
select count(sa)
from StudentAdmin sa
Expand All @@ -31,12 +33,14 @@ Long countByAdminIdBetween(@Param("adminId") Long adminId,
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to);

// 이번 달 신규 가입자 수
default Long countThisMonthByAdminId(Long adminId) {
LocalDateTime from = YearMonth.now().atDay(1).atStartOfDay();
LocalDateTime to = LocalDateTime.now();
return countByAdminIdBetween(adminId, from, to);
}
// 오늘 하루, '나를 admin으로 제휴 맺은 partner'의 제휴를 사용한 '고유 사용자 수'

// 오늘 제휴 사용 고유 사용자 수
@Query(value = """
SELECT COUNT(DISTINCT pu.student_id)
FROM partnership_usage pu
Expand All @@ -48,25 +52,44 @@ SELECT COUNT(DISTINCT pu.student_id)
""", nativeQuery = true)
Long countTodayUsersByAdmin(@Param("adminId") Long adminId);

// 누적: admin이 제휴한 모든 store의 사용 건수 (0건 포함), 사용량 내림차순
@Query(value = """
SELECT
p.id AS paperId,
p.store_id AS storeId,
s.name AS storeName,
CAST(COUNT(pu.id) AS UNSIGNED) AS usageCount
FROM paper p
JOIN store s ON s.id = p.store_id
JOIN paper_content pc ON pc.paper_id = p.id
JOIN partnership_usage pu ON pu.paper_id = pc.id
WHERE p.admin_id = :adminId
GROUP BY p.id, p.store_id, s.name
HAVING usageCount > 0
ORDER BY usageCount DESC, p.id ASC
""", nativeQuery = true)
List<StoreUsageWithPaper> findUsageByStoreWithPaper(@Param("adminId") Long adminId);

// 0건 포함 조회 (대시보드에서 모든 제휴 업체를 보여줘야 하는 경우)
@Query(value = """
SELECT
p.id AS paperId,
p.store_id AS storeId,
s.name AS storeName,
CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount
FROM paper p
JOIN store s ON s.id = p.store_id
LEFT JOIN paper_content pc ON pc.paper_id = p.id
LEFT JOIN partnership_usage pu ON pu.paper_id = pc.id
WHERE p.admin_id = :adminId
GROUP BY p.store_id, s.name
ORDER BY usageCount DESC, storeId ASC
GROUP BY p.id, p.store_id, s.name
ORDER BY usageCount DESC, p.id ASC
""", nativeQuery = true)
List<StoreUsage> findUsageByStore(@Param("adminId") Long adminId);
List<StoreUsageWithPaper> findUsageByStoreIncludingZero(@Param("adminId") Long adminId);

interface StoreUsage {
interface StoreUsageWithPaper {
Long getPaperId(); // 🆕 추가: Paper ID
Long getStoreId();
String getStoreName();
Long getUsageCount();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO;
import com.assu.server.domain.mapping.repository.StudentAdminRepository;
import com.assu.server.domain.partnership.entity.Paper;
import com.assu.server.domain.partnership.repository.PaperRepository;
import com.assu.server.domain.partnership.repository.PartnershipRepository;
import com.assu.server.domain.user.service.StudentService;
import com.assu.server.global.apiPayload.code.status.ErrorStatus;
Expand All @@ -15,76 +16,101 @@
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Transactional
@RequiredArgsConstructor
public class StudentAdminServiceImpl implements StudentAdminService {
private final StudentAdminRepository studentAdminRepository;
private final AdminRepository adminRepository;
private final PartnershipRepository partnershipRepository;
private final PaperRepository paperRepository;

@Override
@Transactional
public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(Long memberId) {

Admin admin = getAdminOrThrow(memberId);
Long total = studentAdminRepository.countAllByAdminId(memberId);
Admin admin = adminRepository.findById(memberId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
String adminName = admin.getName();

return StudentAdminConverter.countAdminAuthDTO(memberId, total, adminName);
return StudentAdminConverter.countAdminAuthDTO(memberId, total, admin.getName());
}

@Override
@Transactional
public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(Long memberId) {

Admin admin = getAdminOrThrow(memberId);
Long total = studentAdminRepository.countThisMonthByAdminId(memberId);
Admin admin = adminRepository.findById(memberId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
String adminName = admin.getName();
return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, adminName);

return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, admin.getName());
}

@Override
@Transactional
public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson(Long memberId) {

Admin admin = getAdminOrThrow(memberId);
Long total = studentAdminRepository.countTodayUsersByAdmin(memberId);
Admin admin = adminRepository.findById(memberId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
String adminName =admin.getName();
return StudentAdminConverter.countUsagePersonDTO(memberId, total, adminName);

return StudentAdminConverter.countUsagePersonDTO(memberId, total, admin.getName());
}

@Override
@Transactional
public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage(Long memberId) {
Admin admin = adminRepository.findById(memberId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
String adminName =admin.getName();
List<StudentAdminRepository.StoreUsage> storeUsages = studentAdminRepository.findUsageByStore(memberId);
Admin admin = getAdminOrThrow(memberId);

List<StudentAdminRepository.StoreUsageWithPaper> storeUsages =
studentAdminRepository.findUsageByStoreWithPaper(memberId);

//예외 처리
if (storeUsages.isEmpty()) {
throw new DatabaseException(ErrorStatus.NO_USAGE_DATA);
}

// 첫 번째가 가장 사용량이 많은 업체 (ORDER BY usageCount DESC)
var top = storeUsages.get(0);
Paper paper = partnershipRepository.findFirstByAdmin_IdAndStore_IdOrderByIdAsc(memberId, top.getStoreId())

Paper paper = paperRepository.findById(top.getPaperId())
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE));
Long total = top.getUsageCount();

return StudentAdminConverter.countUsageResponseDTO(admin, paper, total);
return StudentAdminConverter.countUsageResponseDTO(admin, paper, top.getUsageCount());
}

@Override
@Transactional
public StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList(Long memberId) {
Admin admin = getAdminOrThrow(memberId);

// 🔧 핵심 수정: Paper 정보를 포함한 조회 (N+1 해결)
List<StudentAdminRepository.StoreUsageWithPaper> storeUsages =
studentAdminRepository.findUsageByStoreWithPaper(memberId);

if (storeUsages.isEmpty()) {
// 빈 리스트 반환 (선택: 예외 처리도 가능)
return StudentAdminConverter.countUsageListResponseDTO(List.of());
}

List<Long> paperIds = storeUsages.stream()
.map(StudentAdminRepository.StoreUsageWithPaper::getPaperId)
.toList();

Map<Long, Paper> paperMap = paperRepository.findAllById(paperIds).stream()
.collect(Collectors.toMap(Paper::getId, paper -> paper));

Admin admin = adminRepository.findById(memberId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
List<StudentAdminRepository.StoreUsage> storeUsages = studentAdminRepository.findUsageByStore(memberId);
var items = storeUsages.stream().map(row -> {
Paper paper = partnershipRepository.findFirstByAdmin_IdAndStore_IdOrderByIdAsc(memberId, row.getStoreId())
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE));
Paper paper = paperMap.get(row.getPaperId());
if (paper == null) {
throw new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE);
}
return StudentAdminConverter.countUsageResponseDTO(admin, paper, row.getUsageCount());
}).toList();

return StudentAdminConverter.countUsageListResponseDTO(items);
}

}
// Admin 조회 중복 제거
private Admin getAdminOrThrow(Long adminId) {
return adminRepository.findById(adminId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ public static class WeeklyRankResponseDTO {
private Long rank; // 그 주 순위(1부터)
private Long usageCount; // 그 주 사용 건수
}
@AllArgsConstructor
@RequiredArgsConstructor
@Builder
@Getter
public static class todayBest{
List<String> bestStores;
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand All @@ -35,5 +29,12 @@ public static class ListWeeklyRankResponseDTO {
private String storeName;
private List<WeeklyRankResponseDTO> items; // 과거→현재 (6개)
}
@AllArgsConstructor
@RequiredArgsConstructor
@Builder
@Getter
public static class todayBest{
List<String> bestStores;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
import java.util.List;

public interface StoreRepository extends JpaRepository<Store,Long> {
Optional<Store> findByPartner(Partner partner);

Optional<Store> findByPartner(Partner partner);

Optional<Store> findByNameAndAddressAndDetailAddress(String name, String address, String detailAddress);
// [이번 주] 전체 스토어 중 특정 storeId의 주간 순위/건수 1건

// [이번 주] 전체 스토어 중 특정 storeId의 주간 순위/건수 1건 (ACTIVE만)
@Query(value = """
WITH w AS (
SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AS week_start
Expand All @@ -26,10 +28,9 @@ per_store AS (
CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount,
(SELECT week_start FROM w) AS weekStart
FROM store s
LEFT JOIN paper p ON p.store_id = s.id
LEFT JOIN paper_content pc ON pc.paper_id = p.id
LEFT JOIN paper p ON p.store_id = s.id AND p.is_activated = 'ACTIVE'
LEFT JOIN partnership_usage pu
ON pu.paper_id = pc.id
ON pu.paper_id = p.id
AND pu.created_at >= (SELECT week_start FROM w)
AND pu.created_at < (SELECT week_start FROM w) + INTERVAL 7 DAY
GROUP BY s.id, s.name
Expand All @@ -56,7 +57,7 @@ interface GlobalWeeklyRankRow {
Long getStoreRank();
}

// [최근 6주] 전체 스토어 기준, 특정 storeId의 주간 순위/건수(월요일 시작) 추세
// [최근 6주] 전체 스토어 기준, 특정 storeId의 주간 순위/건수(월요일 시작) 추세 (ACTIVE만)
@Query(value = """
WITH RECURSIVE weeks AS (
SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AS week_start
Expand All @@ -71,11 +72,10 @@ per_store_week AS (
s.name AS storeName,
CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount
FROM weeks w
JOIN store s ON 1=1
LEFT JOIN paper p ON p.store_id = s.id
LEFT JOIN paper_content pc ON pc.paper_id = p.id
JOIN store s ON 1=1
LEFT JOIN paper p ON p.store_id = s.id AND p.is_activated = 'ACTIVE'
LEFT JOIN partnership_usage pu
ON pu.paper_id = pc.id
ON pu.paper_id = p.id
AND pu.created_at >= w.week_start
AND pu.created_at < w.week_start + INTERVAL 7 DAY
GROUP BY w.week_start, s.id, s.name
Expand All @@ -102,7 +102,6 @@ per_store_week AS (
WHERE s.address = :address
AND ((:detail IS NULL AND s.detailAddress IS NULL) OR s.detailAddress = :detail)
""")

Optional<Store> findBySameAddress(
@Param("address") String address,
@Param("detail") String detail
Expand All @@ -119,8 +118,21 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point)
List<Store> findByNameContainingIgnoreCaseOrderByIdDesc(String name);
Optional<Store> findByName(String name);
Optional<Store> findById(Long id);

Optional<Store> findByPartnerId(Long partnerId);


}
// [오늘] 전체 스토어 중 사용 건수 상위 10개 (ACTIVE만)
@Query(value = """
SELECT s.name
FROM store s
LEFT JOIN paper p ON p.store_id = s.id AND p.is_activated = 'ACTIVE'
LEFT JOIN partnership_usage pu
ON pu.paper_id = p.id
AND pu.created_at >= CURDATE()
AND pu.created_at < CURDATE() + INTERVAL 1 DAY
GROUP BY s.id, s.name
HAVING COUNT(pu.id) > 0
ORDER BY COUNT(pu.id) DESC, s.id ASC
LIMIT 10
""", nativeQuery = true)
List<String> findTodayBestStoreNames();
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class StoreServiceImpl implements StoreService {
@Override
@Transactional
public StoreResponseDTO.todayBest getTodayBestStore() {
List<String> bestStores = partnershipUsageRepository.findTodayPopularPartnership();
List<String> bestStores = storeRepository.findTodayBestStoreNames();

return StoreResponseDTO.todayBest.builder()
.bestStores(bestStores)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public enum ErrorStatus implements BaseErrorCode {
REVIEW_REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4003", "자신의 리뷰를 신고할 수 없습니다."),
SUGGESTION_REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4004", "자신의 건의글을 신고할 수 없습니다."),
INVALID_REPORT_TYPE(HttpStatus.BAD_REQUEST, "REPORT_4005", "유효하지 않은 신고 타입입니다."),
;
NO_USAGE_DATA(HttpStatus.NOT_FOUND, "ADMIN4001", "해당 관리자의 제휴 이용 내역이 없습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down