Skip to content
Open
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 @@ -6,6 +6,7 @@
import com.assu.server.global.apiPayload.code.status.SuccessStatus;
import com.assu.server.global.util.PrincipalDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -15,18 +16,21 @@
@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
@Tag(name = "관리자 관련 API", description = "어드민 및 시스템 추천 관련 API")
public class AdminController {

private final AdminService adminService;

@Operation(
summary = "파트너 추천 API",
description = "제휴하지 않은 파트너 중 한 곳을 랜덤으로 조회합니다."
description = "# [v1.0 (2025-09-02)](https://www.notion.so/2591197c19ed80f5b05cffcfecef9c24)\n" +
"- 아직 제휴를 맺지 않은 파트너 중 한 곳을 랜덤으로 추천합니다.\n" +
"- **Authentication**: 헤더에 JWT 토큰 필요 (Admin 권한)"
)
@GetMapping("/partner-recommend")
public BaseResponse<AdminResponseDTO.RandomPartnerResponseDTO> randomPartnerRecommend(
@AuthenticationPrincipal PrincipalDetails pd
) {
) {
return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner(pd.getId()));
}
}
}
40 changes: 26 additions & 14 deletions src/main/java/com/assu/server/domain/admin/entity/Admin.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
package com.assu.server.domain.admin.entity;


import com.assu.server.domain.member.entity.Member;
import com.assu.server.domain.user.entity.enums.Department;
import com.assu.server.domain.user.entity.enums.Major;
import com.assu.server.domain.member.entity.Member;
import com.assu.server.domain.user.entity.enums.University;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Id;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
Expand All @@ -36,37 +27,58 @@ public class Admin {
@OneToOne
@MapsId
@JoinColumn(name = "id")
@NotNull
private Member member;

@NotNull
@Column(nullable = false)
private String name;

@NotNull
@Column(nullable = false)
private String officeAddress;

private String detailAddress;

private String signUrl;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 이미지로 입력받는거니까 signImageUrl로 명시해줍시다


private Boolean isSignVerified;
@NotNull
@Builder.Default
@Column(nullable = false)
private Boolean isSignVerified = false;

private LocalDateTime signVerifiedAt;

@Enumerated(EnumType.STRING)
@NotNull
@Column(nullable = false)
private Major major;

@Enumerated(EnumType.STRING)
@NotNull
@Column(nullable = false)
private Department department;

@Enumerated(EnumType.STRING)
@NotNull
@Column(nullable = false)
private University university;

@JdbcTypeCode(SqlTypes.GEOMETRY)
private Point point;

private double latitude;
private double longitude;
@NotNull
@Column(nullable = false)
private Double latitude;

@NotNull
@Column(nullable = false)
private Double longitude;

public void setMember(Member member) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Setter가 생성해주는 메소드와 겹치니, 해당 메소드를 사용하는 상황에 맞추어서 메소드 명을 변경해주면 좋을 것 같습니다

this.member = member;
if (member != null && member.getId() != null) {
this.id = member.getId();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.assu.server.domain.user.entity.enums.Major;
import com.assu.server.domain.user.entity.enums.University;

// PaperQueryServiceImpl 이 AdminService 참조 중 -> 순환참조 문제 발생하지 않도록 주의
public interface AdminService {
List<Admin> findMatchingAdmins(University university, Department department, Major major);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
package com.assu.server.domain.admin.service;


import java.util.List;
import org.springframework.stereotype.Service;
import com.assu.server.domain.admin.dto.AdminResponseDTO;
import com.assu.server.domain.admin.entity.Admin;
import com.assu.server.domain.admin.repository.AdminRepository;
import com.assu.server.domain.user.entity.enums.Department;
import com.assu.server.domain.user.entity.enums.Major;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import com.assu.server.domain.partner.entity.Partner;
import com.assu.server.domain.partner.repository.PartnerRepository;
import com.assu.server.domain.user.entity.enums.Department;
import com.assu.server.domain.user.entity.enums.Major;
import com.assu.server.domain.user.entity.enums.University;
import com.assu.server.global.apiPayload.code.status.ErrorStatus;
import com.assu.server.global.exception.DatabaseException;
import java.util.concurrent.ThreadLocalRandom;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

@Service
@RequiredArgsConstructor
@Transactional
public class AdminServiceImpl implements AdminService {

private final AdminRepository adminRepository;
private final PartnerRepository partnerRepository;
@Override
@Transactional
public List<Admin> findMatchingAdmins(University university, Department department, Major major){

@Override
@Transactional(readOnly = true)
public List<Admin> findMatchingAdmins(University university, Department department, Major major) {
return adminRepository.findMatchingAdmins(university, department, major);
}

List<Admin> adminList = adminRepository.findMatchingAdmins(university, department,major);

return adminList;
}
@Override
@Transactional
@Transactional(readOnly = true)
public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId) {

Admin admin = adminRepository.findById(adminId)
Expand All @@ -45,10 +43,10 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long admin
throw new DatabaseException(ErrorStatus.NO_AVAILABLE_PARTNER);
}

int offset = ThreadLocalRandom.current().nextInt((int)total);
int offset = ThreadLocalRandom.current().nextInt((int) total);

Partner picked = partnerRepository.findUnpartneredActiveByAdminWithOffset(admin.getId(), offset);
if(picked == null) {
if (picked == null) {
throw new DatabaseException(ErrorStatus.NO_AVAILABLE_PARTNER);
}

Expand All @@ -61,5 +59,4 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long admin
.partnerPhone(picked.getMember().getPhoneNum())
.build();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.assu.server.global.apiPayload.code.status.SuccessStatus;
import com.assu.server.global.util.PrincipalDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -15,21 +16,27 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/admin/dashBoard")
@Tag(name = "관리자 대시보드 API", description = "어드민 전용 통계 및 대시보드 데이터 조회 API")
public class StudentAdminController {

private final StudentAdminService studentAdminService;

@Operation(
summary = "누적 가입자 수 조회 API",
description = "admin으로 접근해주세요."
description = "# [v1.0 (2025-09-02)](https://www.notion.so/_-24c1197c19ed8062be94fc08619b760f)\n" +
"- 관리자(Admin) 권한으로 접근하여 현재까지의 총 누적 가입자 수를 조회합니다."
)
@GetMapping
public BaseResponse<StudentAdminResponseDTO.CountAdminAuthResponseDTO> getCountAdmin(
@AuthenticationPrincipal PrincipalDetails pd
) {
) {
return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth(pd.getId()));
}

@Operation(
summary = "신규 한 달 가입자 수 조회 API",
description = "admin으로 접근해주세요."
description = "# [v1.0 (2025-09-02)](https://www.notion.so/_-24c1197c19ed805db80fca98c38849d1)\n" +
"- 이번 달(매달 1일 초기화) 기준 신규 가입한 사용자 수를 조회합니다."
)
@GetMapping("/new")
public BaseResponse<StudentAdminResponseDTO.NewCountAdminResponseDTO> getNewStudentCountAdmin(
Expand All @@ -40,37 +47,37 @@ public BaseResponse<StudentAdminResponseDTO.NewCountAdminResponseDTO> getNewStud

@Operation(
summary = "오늘 제휴 사용자 수 조회 API",
description = "admin으로 접근해주세요."
description = "# [v1.0 (2025-09-02)](https://www.notion.so/_-24e1197c19ed80a283b1c336a1c3df72)\n" +
"- 금일 제휴 서비스를 이용한 총 사용자 수를 조회합니다."
)
@GetMapping("/countUser")
public BaseResponse<StudentAdminResponseDTO.CountUsagePersonResponseDTO> getCountUser(
@AuthenticationPrincipal PrincipalDetails pd
){
return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson(pd.getId()));
}

@Operation(
summary = "제휴업체 누적별 1위 업체 조회 API",
description = "adminId로 접근해주세요."
description = "# [v1.0 (2025-09-02)](https://www.notion.so/_1-2ef1197c19ed8010a49ce6313d137b4f)\n" +
"- 제휴 이용 횟수가 가장 많은 1위 업체의 정보를 조회합니다."
)
@GetMapping("/top")
public BaseResponse<StudentAdminResponseDTO.CountUsageResponseDTO> getTopUsage(
@AuthenticationPrincipal PrincipalDetails pd
@GetMapping("/top")
public BaseResponse<StudentAdminResponseDTO.CountUsageResponseDTO> getTopUsage(
@AuthenticationPrincipal PrincipalDetails pd
) {
return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage(pd.getId()));
}

/**
* 제휴 업체별 누적 제휴 이용 현황 리스트 반환 (사용량 내림차순)
*/
@Operation(
summary = "제휴업체 누적 사용 수 내림차순 조회 API",
description = "adminId로 접근해주세요."
)
@GetMapping("/usage")
public BaseResponse<StudentAdminResponseDTO.CountUsageListResponseDTO> getUsageList(
@AuthenticationPrincipal PrincipalDetails pd
) {
return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList(pd.getId()));
}
return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage(pd.getId()));
}

}
@Operation(
summary = "제휴업체 누적 사용 수 내림차순 조회 API",
description = "# [v1.0 (2025-09-02)](https://www.notion.so/_-24e1197c19ed802b92eff5d4dc4dbe82)\n" +
"- 모든 제휴 업체의 누적 사용 현황을 사용량 내림차순 리스트로 반환합니다."
)
@GetMapping("/usage")
public BaseResponse<StudentAdminResponseDTO.CountUsageListResponseDTO> getUsageList(
@AuthenticationPrincipal PrincipalDetails pd
) {
return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList(pd.getId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@

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 @@ -33,14 +31,12 @@ 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);
}

// 오늘 제휴 사용 고유 사용자 수
@Query(value = """
SELECT COUNT(DISTINCT pu.student_id)
FROM partnership_usage pu
Expand Down Expand Up @@ -69,7 +65,6 @@ SELECT COUNT(DISTINCT pu.student_id)
""", nativeQuery = true)
List<StoreUsageWithPaper> findUsageByStoreWithPaper(@Param("adminId") Long adminId);

// 0건 포함 조회 (대시보드에서 모든 제휴 업체를 보여줘야 하는 경우)
@Query(value = """
SELECT
p.id AS paperId,
Expand All @@ -87,7 +82,7 @@ SELECT COUNT(DISTINCT pu.student_id)
List<StoreUsageWithPaper> findUsageByStoreIncludingZero(@Param("adminId") Long adminId);

interface StoreUsageWithPaper {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유지보수를 고려하여서 record 기반의 DTO로 리팩토링합시다

Long getPaperId(); // 🆕 추가: Paper ID
Long getPaperId();
Long getStoreId();
String getStoreName();
Long getUsageCount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@
import com.assu.server.global.apiPayload.code.status.SuccessStatus;
import com.assu.server.global.util.PrincipalDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/partner")
@RequiredArgsConstructor
@Tag(name = "파트너 관련 API", description = "파트너 전용 기능 및 추천 관련 API")
public class PartnerController {

private final PartnerService partnerService;

@Operation(
summary = "어드민 추천 API",
description = "제휴하지 않은 어드민 중 두 곳을 랜덤으로 조회합니다."
description = "# [v1.0 (2025-09-02)](https://www.notion.so/2591197c19ed80368a9edf1f6e92ea38)\n" +
"- 아직 제휴를 맺지 않은 어드민(학교/단체) 중 두 곳을 랜덤으로 조회합니다.\n" +
"- **Authentication**: 헤더에 JWT 토큰 필요 (Partner 권한)"
)
@GetMapping("/admin-recommend")
public BaseResponse<PartnerResponseDTO.RandomAdminResponseDTO> randomAdminRecommend(
@AuthenticationPrincipal PrincipalDetails pd
){
){
return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin(pd.getId()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.assu.server.domain.partner.repository;

import com.assu.server.domain.common.enums.ActivationStatus;
import com.assu.server.domain.partner.entity.Partner;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

Expand Down
Loading