Skip to content

Commit 9151b7d

Browse files
authored
Merge pull request #91 from focus-to-level-up/admin-page
feat: implement admin page apis
2 parents ebf5278 + ea7626a commit 9151b7d

36 files changed

Lines changed: 1525 additions & 0 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.studioedge.focus_to_levelup_server.domain.admin.controller;
2+
3+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminUpdateGuildDescriptionRequest;
4+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminUpdateGuildNameRequest;
5+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.AdminGuildResponse;
6+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminAuthService;
7+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminGuildService;
8+
import com.studioedge.focus_to_levelup_server.domain.member.entity.Member;
9+
import com.studioedge.focus_to_levelup_server.global.response.CommonResponse;
10+
import com.studioedge.focus_to_levelup_server.global.response.HttpResponseUtil;
11+
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.Parameter;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import jakarta.validation.Valid;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
18+
import org.springframework.web.bind.annotation.*;
19+
20+
@Tag(name = "Admin - Guild", description = "관리자 길드 관리 API")
21+
@RestController
22+
@RequestMapping("/api/v1/admin/guilds")
23+
@RequiredArgsConstructor
24+
public class AdminGuildController {
25+
26+
private final AdminAuthService adminAuthService;
27+
private final AdminGuildService adminGuildService;
28+
29+
@GetMapping("/{guildId}")
30+
@Operation(summary = "길드 조회", description = "길드 ID로 길드 정보를 조회합니다.")
31+
public ResponseEntity<CommonResponse<AdminGuildResponse>> getGuild(
32+
@AuthenticationPrincipal Member member,
33+
@Parameter(description = "조회할 길드 ID") @PathVariable Long guildId
34+
) {
35+
adminAuthService.validateAdminAccess(member.getId());
36+
return HttpResponseUtil.ok(adminGuildService.getGuildById(guildId));
37+
}
38+
39+
@PutMapping("/{guildId}/name")
40+
@Operation(summary = "길드명 변경", description = "길드명을 변경합니다.")
41+
public ResponseEntity<CommonResponse<AdminGuildResponse>> updateGuildName(
42+
@AuthenticationPrincipal Member member,
43+
@Parameter(description = "변경할 길드 ID") @PathVariable Long guildId,
44+
@Valid @RequestBody AdminUpdateGuildNameRequest request
45+
) {
46+
adminAuthService.validateAdminAccess(member.getId());
47+
return HttpResponseUtil.ok(adminGuildService.updateGuildName(guildId, request.name()));
48+
}
49+
50+
@PutMapping("/{guildId}/description")
51+
@Operation(summary = "길드 설명 변경", description = "길드 설명(상태메시지)을 변경합니다.")
52+
public ResponseEntity<CommonResponse<AdminGuildResponse>> updateGuildDescription(
53+
@AuthenticationPrincipal Member member,
54+
@Parameter(description = "변경할 길드 ID") @PathVariable Long guildId,
55+
@Valid @RequestBody AdminUpdateGuildDescriptionRequest request
56+
) {
57+
adminAuthService.validateAdminAccess(member.getId());
58+
return HttpResponseUtil.ok(adminGuildService.updateGuildDescription(guildId, request.description()));
59+
}
60+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.studioedge.focus_to_levelup_server.domain.admin.controller;
2+
3+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminSendMailRequest;
4+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminSendPreRegistrationRequest;
5+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.AdminMailResponse;
6+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminAuthService;
7+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminMailService;
8+
import com.studioedge.focus_to_levelup_server.domain.member.entity.Member;
9+
import com.studioedge.focus_to_levelup_server.global.response.CommonResponse;
10+
import com.studioedge.focus_to_levelup_server.global.response.HttpResponseUtil;
11+
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.tags.Tag;
13+
import jakarta.validation.Valid;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
@Tag(name = "Admin - Mail", description = "관리자 우편 관리 API (재화 지급)")
20+
@RestController
21+
@RequestMapping("/api/v1/admin/mails")
22+
@RequiredArgsConstructor
23+
public class AdminMailController {
24+
25+
private final AdminAuthService adminAuthService;
26+
private final AdminMailService adminMailService;
27+
28+
@PostMapping
29+
@Operation(summary = "재화 지급 우편 발송", description = "유저에게 다이아/골드/보너스티켓을 우편으로 지급합니다.")
30+
public ResponseEntity<CommonResponse<AdminMailResponse>> sendRewardMail(
31+
@AuthenticationPrincipal Member member,
32+
@Valid @RequestBody AdminSendMailRequest request
33+
) {
34+
adminAuthService.validateAdminAccess(member.getId());
35+
return HttpResponseUtil.created(adminMailService.sendRewardMail(request));
36+
}
37+
38+
@PostMapping("/pre-registration")
39+
@Operation(summary = "사전예약 패키지 지급", description = "사전예약 보상을 지급합니다. (다이아 500 + 보너스티켓 3개 + 캐릭터 선택권)")
40+
public ResponseEntity<CommonResponse<AdminMailResponse>> sendPreRegistrationPackage(
41+
@AuthenticationPrincipal Member member,
42+
@Valid @RequestBody AdminSendPreRegistrationRequest request
43+
) {
44+
adminAuthService.validateAdminAccess(member.getId());
45+
return HttpResponseUtil.created(
46+
adminMailService.sendPreRegistrationPackage(
47+
request.receiverId(),
48+
request.customTitle(),
49+
request.customDescription()
50+
)
51+
);
52+
}
53+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.studioedge.focus_to_levelup_server.domain.admin.controller;
2+
3+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminUpdateNicknameRequest;
4+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminUpdateProfileMessageRequest;
5+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.request.AdminUpdateSchoolRequest;
6+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.AdminMemberResponse;
7+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminAuthService;
8+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminMemberService;
9+
import com.studioedge.focus_to_levelup_server.domain.member.entity.Member;
10+
import com.studioedge.focus_to_levelup_server.global.response.CommonResponse;
11+
import com.studioedge.focus_to_levelup_server.global.response.HttpResponseUtil;
12+
import io.swagger.v3.oas.annotations.Operation;
13+
import io.swagger.v3.oas.annotations.Parameter;
14+
import io.swagger.v3.oas.annotations.tags.Tag;
15+
import jakarta.validation.Valid;
16+
import lombok.RequiredArgsConstructor;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
@Tag(name = "Admin - Member", description = "관리자 회원 관리 API")
22+
@RestController
23+
@RequestMapping("/api/v1/admin/members")
24+
@RequiredArgsConstructor
25+
public class AdminMemberController {
26+
27+
private final AdminAuthService adminAuthService;
28+
private final AdminMemberService adminMemberService;
29+
30+
@GetMapping("/search")
31+
@Operation(summary = "닉네임으로 회원 검색", description = "닉네임으로 회원을 검색합니다. (RevenueCat 연동용 ID 확인)")
32+
public ResponseEntity<CommonResponse<AdminMemberResponse>> searchMember(
33+
@AuthenticationPrincipal Member member,
34+
@Parameter(description = "검색할 닉네임") @RequestParam String nickname
35+
) {
36+
adminAuthService.validateAdminAccess(member.getId());
37+
return HttpResponseUtil.ok(adminMemberService.searchMemberByNickname(nickname));
38+
}
39+
40+
@GetMapping("/{memberId}")
41+
@Operation(summary = "회원 ID로 조회", description = "회원 ID로 상세 정보를 조회합니다.")
42+
public ResponseEntity<CommonResponse<AdminMemberResponse>> getMember(
43+
@AuthenticationPrincipal Member member,
44+
@Parameter(description = "조회할 회원 ID") @PathVariable Long memberId
45+
) {
46+
adminAuthService.validateAdminAccess(member.getId());
47+
return HttpResponseUtil.ok(adminMemberService.getMemberById(memberId));
48+
}
49+
50+
@PutMapping("/{memberId}/nickname")
51+
@Operation(summary = "닉네임 변경", description = "회원의 닉네임을 변경합니다. (1달 제한 무시)")
52+
public ResponseEntity<CommonResponse<AdminMemberResponse>> updateNickname(
53+
@AuthenticationPrincipal Member member,
54+
@Parameter(description = "변경할 회원 ID") @PathVariable Long memberId,
55+
@Valid @RequestBody AdminUpdateNicknameRequest request
56+
) {
57+
adminAuthService.validateAdminAccess(member.getId());
58+
return HttpResponseUtil.ok(adminMemberService.updateNickname(memberId, request.nickname()));
59+
}
60+
61+
@PutMapping("/{memberId}/profile-message")
62+
@Operation(summary = "상태메시지 변경", description = "회원의 상태메시지를 변경합니다.")
63+
public ResponseEntity<CommonResponse<AdminMemberResponse>> updateProfileMessage(
64+
@AuthenticationPrincipal Member member,
65+
@Parameter(description = "변경할 회원 ID") @PathVariable Long memberId,
66+
@Valid @RequestBody AdminUpdateProfileMessageRequest request
67+
) {
68+
adminAuthService.validateAdminAccess(member.getId());
69+
return HttpResponseUtil.ok(adminMemberService.updateProfileMessage(memberId, request.profileMessage()));
70+
}
71+
72+
@PutMapping("/{memberId}/school")
73+
@Operation(summary = "학교 정보 변경", description = "회원의 학교 정보를 변경합니다.")
74+
public ResponseEntity<CommonResponse<AdminMemberResponse>> updateSchool(
75+
@AuthenticationPrincipal Member member,
76+
@Parameter(description = "변경할 회원 ID") @PathVariable Long memberId,
77+
@Valid @RequestBody AdminUpdateSchoolRequest request
78+
) {
79+
adminAuthService.validateAdminAccess(member.getId());
80+
return HttpResponseUtil.ok(adminMemberService.updateSchool(memberId, request.school(), request.schoolAddress()));
81+
}
82+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.studioedge.focus_to_levelup_server.domain.admin.controller;
2+
3+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.AdminReportResponse;
4+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminAuthService;
5+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminReportService;
6+
import com.studioedge.focus_to_levelup_server.domain.member.entity.Member;
7+
import com.studioedge.focus_to_levelup_server.global.response.CommonResponse;
8+
import com.studioedge.focus_to_levelup_server.global.response.HttpResponseUtil;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.Parameter;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.data.domain.Page;
14+
import org.springframework.data.domain.Pageable;
15+
import org.springframework.data.web.PageableDefault;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
18+
import org.springframework.web.bind.annotation.*;
19+
20+
@Tag(name = "Admin - Report", description = "관리자 신고 관리 API")
21+
@RestController
22+
@RequestMapping("/api/v1/admin/reports")
23+
@RequiredArgsConstructor
24+
public class AdminReportController {
25+
26+
private final AdminAuthService adminAuthService;
27+
private final AdminReportService adminReportService;
28+
29+
@GetMapping
30+
@Operation(summary = "신고 목록 조회", description = "신고 목록을 조회합니다. 피신고자의 닉네임, 상태메시지, 총 신고 수를 확인할 수 있습니다.")
31+
public ResponseEntity<CommonResponse<Page<AdminReportResponse>>> getReportList(
32+
@AuthenticationPrincipal Member member,
33+
@Parameter(description = "페이지 정보 (page, size, sort)")
34+
@PageableDefault(size = 20) Pageable pageable
35+
) {
36+
adminAuthService.validateAdminAccess(member.getId());
37+
return HttpResponseUtil.ok(adminReportService.getReportList(pageable));
38+
}
39+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.studioedge.focus_to_levelup_server.domain.admin.controller;
2+
3+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.CategoryDistributionResponse;
4+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.FocusTimeDistributionResponse;
5+
import com.studioedge.focus_to_levelup_server.domain.admin.dto.response.GenderDistributionResponse;
6+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminAuthService;
7+
import com.studioedge.focus_to_levelup_server.domain.admin.service.AdminStatsService;
8+
import com.studioedge.focus_to_levelup_server.domain.member.entity.Member;
9+
import com.studioedge.focus_to_levelup_server.global.response.CommonResponse;
10+
import com.studioedge.focus_to_levelup_server.global.response.HttpResponseUtil;
11+
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.Parameter;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.format.annotation.DateTimeFormat;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
18+
import org.springframework.web.bind.annotation.*;
19+
20+
import java.time.LocalDate;
21+
22+
@Tag(name = "Admin - Stats", description = "관리자 통계 API")
23+
@RestController
24+
@RequestMapping("/api/v1/admin/stats")
25+
@RequiredArgsConstructor
26+
public class AdminStatsController {
27+
28+
private final AdminAuthService adminAuthService;
29+
private final AdminStatsService adminStatsService;
30+
31+
@GetMapping("/focus-time/daily")
32+
@Operation(summary = "일간 집중시간 분포", description = "특정 날짜의 집중시간 분포를 조회합니다. (2시간 단위: 0~2, 2~4, 4~6, 6~8, 8~10, 10시간 이상)")
33+
public ResponseEntity<CommonResponse<FocusTimeDistributionResponse>> getDailyFocusTimeDistribution(
34+
@AuthenticationPrincipal Member member,
35+
@Parameter(description = "조회 날짜 (기본값: 오늘)", example = "2024-03-21")
36+
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date
37+
) {
38+
adminAuthService.validateAdminAccess(member.getId());
39+
LocalDate targetDate = date != null ? date : LocalDate.now();
40+
return HttpResponseUtil.ok(adminStatsService.getDailyFocusTimeDistribution(targetDate));
41+
}
42+
43+
@GetMapping("/focus-time/weekly")
44+
@Operation(summary = "주간 집중시간 분포", description = "특정 날짜가 속한 주의 집중시간 분포를 조회합니다. (5시간 단위: 0~5, 5~10, ..., 50시간 이상)")
45+
public ResponseEntity<CommonResponse<FocusTimeDistributionResponse>> getWeeklyFocusTimeDistribution(
46+
@AuthenticationPrincipal Member member,
47+
@Parameter(description = "조회 기준 날짜 (해당 주 조회, 기본값: 오늘)", example = "2024-03-21")
48+
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date
49+
) {
50+
adminAuthService.validateAdminAccess(member.getId());
51+
LocalDate targetDate = date != null ? date : LocalDate.now();
52+
return HttpResponseUtil.ok(adminStatsService.getWeeklyFocusTimeDistribution(targetDate));
53+
}
54+
55+
@GetMapping("/category")
56+
@Operation(summary = "카테고리 분포", description = "카테고리별 유저 수 및 비율을 조회합니다.")
57+
public ResponseEntity<CommonResponse<CategoryDistributionResponse>> getCategoryDistribution(
58+
@AuthenticationPrincipal Member member
59+
) {
60+
adminAuthService.validateAdminAccess(member.getId());
61+
return HttpResponseUtil.ok(adminStatsService.getCategoryDistribution());
62+
}
63+
64+
@GetMapping("/gender")
65+
@Operation(summary = "성별 분포", description = "성별 유저 수 및 비율을 조회합니다.")
66+
public ResponseEntity<CommonResponse<GenderDistributionResponse>> getGenderDistribution(
67+
@AuthenticationPrincipal Member member
68+
) {
69+
adminAuthService.validateAdminAccess(member.getId());
70+
return HttpResponseUtil.ok(adminStatsService.getGenderDistribution());
71+
}
72+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.studioedge.focus_to_levelup_server.domain.admin.dao;
2+
3+
import com.studioedge.focus_to_levelup_server.domain.admin.entity.AdminWhitelist;
4+
import com.studioedge.focus_to_levelup_server.domain.admin.enums.AdminRole;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
import org.springframework.stereotype.Repository;
9+
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
@Repository
14+
public interface AdminWhitelistRepository extends JpaRepository<AdminWhitelist, Long> {
15+
16+
/**
17+
* Member ID로 Admin 조회
18+
*/
19+
Optional<AdminWhitelist> findByMemberId(Long memberId);
20+
21+
/**
22+
* Member ID로 Admin 존재 여부 확인
23+
*/
24+
boolean existsByMemberId(Long memberId);
25+
26+
/**
27+
* Member ID와 Role로 Admin 존재 여부 확인
28+
*/
29+
boolean existsByMemberIdAndRole(Long memberId, AdminRole role);
30+
31+
/**
32+
* 전체 Admin 목록 조회 (Member 정보 포함)
33+
*/
34+
@Query("SELECT a FROM AdminWhitelist a JOIN FETCH a.member")
35+
List<AdminWhitelist> findAllWithMember();
36+
37+
/**
38+
* Role별 Admin 목록 조회
39+
*/
40+
@Query("SELECT a FROM AdminWhitelist a JOIN FETCH a.member WHERE a.role = :role")
41+
List<AdminWhitelist> findAllByRoleWithMember(@Param("role") AdminRole role);
42+
43+
/**
44+
* Member ID로 삭제
45+
*/
46+
void deleteByMemberId(Long memberId);
47+
}

0 commit comments

Comments
 (0)