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
@@ -0,0 +1,24 @@
package redot.redot_server.domain.admin.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redot.redot_server.domain.admin.controller.docs.AdminDashboardControllerDocs;
import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse;
import redot.redot_server.domain.admin.service.AdminDashboardService;

@RestController
@RequestMapping("/api/v1/redot/admin/dashboard")
@RequiredArgsConstructor
public class AdminDashboardController implements AdminDashboardControllerDocs {

private final AdminDashboardService adminDashboardService;

@GetMapping("/stats")
@Override
public ResponseEntity<AdminDashboardStatsResponse> getDashboardStats() {
return ResponseEntity.ok(adminDashboardService.getDashboardStats());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redot.redot_server.domain.admin.controller.docs.AdminRedotAppControllerDocs;
import redot.redot_server.domain.admin.dto.request.RedotAppInfoSearchCondition;
import redot.redot_server.domain.admin.dto.request.RedotAppStatusUpdateRequest;
import redot.redot_server.domain.admin.service.AdminRedotAppService;
import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest;
import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse;
import redot.redot_server.global.util.dto.response.PageResponse;

@RestController
@RequestMapping("/api/v1/redot/admin/app")
Expand All @@ -24,4 +33,27 @@ public class AdminRedotAppController implements AdminRedotAppControllerDocs {
public ResponseEntity<RedotAppInfoResponse> createRedotApp(@Valid @RequestBody RedotAppCreateRequest request) {
return ResponseEntity.ok(redotAppService.createRedotApp(request));
}

@GetMapping
@Override
public ResponseEntity<PageResponse<RedotAppInfoResponse>> getRedotAppInfoList(
@ParameterObject RedotAppInfoSearchCondition searchCondition,
@ParameterObject @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok(redotAppService.getRedotAppInfoList(searchCondition, pageable));
}

@GetMapping("/{redotAppId}")
@Override
public ResponseEntity<RedotAppInfoResponse> getRedotAppInfo(
@PathVariable("redotAppId") Long redotAppId) {
return ResponseEntity.ok(redotAppService.getRedotAppInfo(redotAppId));
}

@PostMapping("/{redotAppId}/status")
@Override
public ResponseEntity<RedotAppInfoResponse> updateRedotAppStatus(
@PathVariable("redotAppId") Long redotAppId,
@Valid @RequestBody RedotAppStatusUpdateRequest request) {
return ResponseEntity.ok(redotAppService.updateRedotAppStatus(redotAppId, request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package redot.redot_server.domain.admin.controller.docs;

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.tags.Tag;
import org.springframework.http.ResponseEntity;
import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse;

@Tag(name = "Admin Dashboard", description = "관리자 대시보드 통계 API")
public interface AdminDashboardControllerDocs {

@Operation(summary = "대시보드 통계 조회", description = "관리자 대시보드에 필요한 요약 통계를 제공합니다.")
@ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(schema = @Schema(implementation = AdminDashboardStatsResponse.class)))
ResponseEntity<AdminDashboardStatsResponse> getDashboardStats();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import redot.redot_server.domain.admin.dto.request.RedotAppInfoSearchCondition;
import redot.redot_server.domain.admin.dto.request.RedotAppStatusUpdateRequest;
import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest;
import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse;
import redot.redot_server.global.util.dto.response.PageResponse;

@Tag(name = "Admin Redot App", description = "관리자 앱 관리 API")
public interface AdminRedotAppControllerDocs {
Expand All @@ -17,4 +22,24 @@ public interface AdminRedotAppControllerDocs {
@ApiResponse(responseCode = "200", description = "생성 성공",
content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class)))
ResponseEntity<RedotAppInfoResponse> createRedotApp(@Valid RedotAppCreateRequest request);

@Operation(summary = "앱 목록 조회", description = "status, name, redot_member_id 파라미터로 필터링하며 createdAt 기준 최신순 정렬을 기본으로 제공합니다.")
@ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(schema = @Schema(implementation = PageResponse.class)))
ResponseEntity<PageResponse<RedotAppInfoResponse>> getRedotAppInfoList(
@ParameterObject RedotAppInfoSearchCondition searchCondition,
@ParameterObject Pageable pageable);

@Operation(summary = "앱 단건 조회", description = "redotAppId로 특정 앱 정보를 조회합니다.")
@ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class)))
ResponseEntity<RedotAppInfoResponse> getRedotAppInfo(
@io.swagger.v3.oas.annotations.Parameter(description = "Redot 앱 ID", example = "1") Long redotAppId);

@Operation(summary = "앱 상태 변경", description = "지정한 앱의 상태 및 비고(remark)를 수정합니다.")
@ApiResponse(responseCode = "200", description = "수정 성공",
content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class)))
ResponseEntity<RedotAppInfoResponse> updateRedotAppStatus(
@io.swagger.v3.oas.annotations.Parameter(description = "Redot 앱 ID", example = "1") Long redotAppId,
@Valid RedotAppStatusUpdateRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package redot.redot_server.domain.admin.dto.request;

import redot.redot_server.domain.redot.app.entity.RedotAppStatus;

public record RedotAppInfoSearchCondition(
String name,
Long redotMemberId,
RedotAppStatus status
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package redot.redot_server.domain.admin.dto.request;

import jakarta.validation.constraints.NotNull;
import redot.redot_server.domain.redot.app.entity.RedotAppStatus;

public record RedotAppStatusUpdateRequest(
@NotNull(message = "status 는 필수입니다.") RedotAppStatus status,
String remark
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package redot.redot_server.domain.admin.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record AdminDashboardStatsResponse(
@Schema(description = "전체 Redot 회원 수") long totalRedotMembers,
@Schema(description = "전일까지 누적된 Redot 회원 수") long redotMembersUntilYesterday,
@Schema(description = "상태가 PENDING 인 상담 요청 수") long pendingConsultationCount,
@Schema(description = "등록된 관리자 수") long adminCount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package redot.redot_server.domain.admin.service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse;
import redot.redot_server.domain.admin.repository.AdminRepository;
import redot.redot_server.domain.redot.consultation.entity.ConsultationStatus;
import redot.redot_server.domain.redot.consultation.repository.ConsultationRepository;
import redot.redot_server.domain.redot.member.repository.RedotMemberRepository;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AdminDashboardService {

private final RedotMemberRepository redotMemberRepository;
private final ConsultationRepository consultationRepository;
private final AdminRepository adminRepository;

public AdminDashboardStatsResponse getDashboardStats() {
ZoneId kstZone = ZoneId.of("Asia/Seoul");
LocalDateTime startOfTodayKst = LocalDate.now(kstZone).atStartOfDay();
LocalDateTime startOfTodayUtc = startOfTodayKst.atZone(kstZone)
.withZoneSameInstant(ZoneOffset.UTC)
.toLocalDateTime();

long totalRedotMembers = redotMemberRepository.count();
long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfTodayUtc);

long pendingConsultationCount = consultationRepository.countByStatus(ConsultationStatus.PENDING);
long adminCount = adminRepository.count();

return new AdminDashboardStatsResponse(
totalRedotMembers,
redotMembersUntilYesterday,
pendingConsultationCount,
adminCount
);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,128 @@
package redot.redot_server.domain.admin.service;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import redot.redot_server.domain.admin.dto.request.RedotAppInfoSearchCondition;
import redot.redot_server.domain.admin.dto.request.RedotAppStatusUpdateRequest;
import redot.redot_server.domain.cms.site.setting.dto.response.SiteSettingResponse;
import redot.redot_server.domain.cms.site.style.dto.response.StyleInfoResponse;
import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest;
import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse;
import redot.redot_server.domain.redot.app.dto.response.RedotAppResponse;
import redot.redot_server.domain.redot.app.entity.RedotApp;
import redot.redot_server.domain.redot.app.exception.RedotAppErrorCode;
import redot.redot_server.domain.redot.app.exception.RedotAppException;
import redot.redot_server.domain.redot.app.repository.RedotAppRepository;
import redot.redot_server.domain.redot.app.service.RedotAppCreationService;
import redot.redot_server.domain.redot.member.dto.response.RedotMemberResponse;
import redot.redot_server.domain.site.domain.entity.Domain;
import redot.redot_server.domain.site.domain.repository.DomainRepository;
import redot.redot_server.domain.site.setting.entity.SiteSetting;
import redot.redot_server.domain.site.setting.repository.SiteSettingRepository;
import redot.redot_server.domain.site.style.entity.StyleInfo;
import redot.redot_server.domain.site.style.repository.StyleInfoRepository;
import redot.redot_server.global.s3.util.ImageUrlResolver;
import redot.redot_server.global.util.dto.response.PageResponse;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AdminRedotAppService {
private final RedotAppCreationService redotAppCreationService;
private final RedotAppRepository redotAppRepository;
private final DomainRepository domainRepository;
private final SiteSettingRepository siteSettingRepository;
private final StyleInfoRepository styleInfoRepository;
private final ImageUrlResolver imageUrlResolver;

@Transactional
public RedotAppInfoResponse createRedotApp(RedotAppCreateRequest request) {
return redotAppCreationService.createWithoutOwner(request);
}

public PageResponse<RedotAppInfoResponse> getRedotAppInfoList(RedotAppInfoSearchCondition searchCondition,
Pageable pageable) {
Page<RedotApp> redotApps = redotAppRepository.findAllBySearchCondition(searchCondition, pageable);

List<Long> redotAppIds = redotApps.stream()
.map(RedotApp::getId)
.toList();

Map<Long, Domain> domainMap = redotAppIds.isEmpty() ? Map.of()
: domainRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(domain -> domain.getRedotApp().getId(), Function.identity()));

Map<Long, StyleInfo> styleInfoMap = redotAppIds.isEmpty() ? Map.of()
: styleInfoRepository.findByRedotApp_IdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(style -> style.getRedotApp().getId(), Function.identity()));

Map<Long, SiteSetting> siteSettingMap = redotAppIds.isEmpty() ? Map.of()
: siteSettingRepository.findByRedotAppIdIn(redotAppIds)
.stream()
.collect(Collectors.toMap(setting -> setting.getRedotApp().getId(), Function.identity()));

Page<RedotAppInfoResponse> responsePage = redotApps.map(redotApp ->
toRedotAppInfoResponse(
redotApp,
domainMap.get(redotApp.getId()),
styleInfoMap.get(redotApp.getId()),
siteSettingMap.get(redotApp.getId())
));

return PageResponse.from(responsePage);
}

private RedotAppInfoResponse toRedotAppInfoResponse(RedotApp redotApp) {
Domain domain = domainRepository.findByRedotAppId(redotApp.getId()).orElse(null);
StyleInfo styleInfo = styleInfoRepository.findByRedotApp_Id(redotApp.getId()).orElse(null);
SiteSetting siteSetting = siteSettingRepository.findByRedotAppId(redotApp.getId()).orElse(null);

return toRedotAppInfoResponse(redotApp, domain, styleInfo, siteSetting);
}

private RedotAppInfoResponse toRedotAppInfoResponse(RedotApp redotApp,
Domain domain,
StyleInfo styleInfo,
SiteSetting siteSetting) {
SiteSettingResponse siteSettingResponse = (siteSetting != null && domain != null)
? SiteSettingResponse.fromEntity(siteSetting, domain, imageUrlResolver)
: null;

StyleInfoResponse styleInfoResponse = styleInfo != null
? StyleInfoResponse.fromEntity(styleInfo)
: null;

return new RedotAppInfoResponse(
RedotAppResponse.fromEntity(redotApp),
siteSettingResponse,
styleInfoResponse,
RedotMemberResponse.fromNullable(redotApp.getOwner(), imageUrlResolver)
);
}

public RedotAppInfoResponse getRedotAppInfo(Long redotAppId) {
RedotApp redotApp = redotAppRepository.findById(redotAppId)
.orElseThrow(() -> new RedotAppException(RedotAppErrorCode.REDOT_APP_NOT_FOUND));

return toRedotAppInfoResponse(redotApp);
}

@Transactional
public RedotAppInfoResponse updateRedotAppStatus(Long redotAppId, RedotAppStatusUpdateRequest request) {
RedotApp redotApp = redotAppRepository.findById(redotAppId)
.orElseThrow(() -> new RedotAppException(RedotAppErrorCode.REDOT_APP_NOT_FOUND));

redotApp.updateStatus(request.status(), request.remark());

return toRedotAppInfoResponse(redotApp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ public class AdminImpersonationController implements AdminImpersonationControlle

@PostMapping("/cms-admin")
@Override
public ResponseEntity<TokenResponse> impersonateAsCMSAdmin(HttpServletRequest request, @Valid @RequestBody CMSAdminImpersonationRequest cmsAdminImpersonationRequest, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) {
Long adminId = jwtPrincipal.id();

AuthResult authResult = adminImpersonationService.impersonateAsCMSAdmin(request, cmsAdminImpersonationRequest, adminId);
public ResponseEntity<TokenResponse> impersonateAsCMSAdmin(HttpServletRequest request, @Valid @RequestBody CMSAdminImpersonationRequest cmsAdminImpersonationRequest) {
AuthResult authResult = adminImpersonationService.impersonateAsCMSAdmin(request, cmsAdminImpersonationRequest);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, authResult.accessCookie().toString())
.header(HttpHeaders.SET_COOKIE, authResult.refreshCookie().toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.springframework.http.ResponseEntity;
import redot.redot_server.domain.auth.dto.request.CMSAdminImpersonationRequest;
import redot.redot_server.domain.auth.dto.response.TokenResponse;
import redot.redot_server.global.security.principal.JwtPrincipal;

@Tag(name = "Admin Impersonation", description = "관리자 권한 위임 API")
public interface AdminImpersonationControllerDocs {
Expand All @@ -20,6 +19,5 @@ public interface AdminImpersonationControllerDocs {
@ApiResponse(responseCode = "200", description = "발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class)))
ResponseEntity<TokenResponse> impersonateAsCMSAdmin(@Parameter(hidden = true) HttpServletRequest request,
@Valid CMSAdminImpersonationRequest cmsAdminImpersonationRequest,
@Parameter(hidden = true) JwtPrincipal jwtPrincipal);
@Valid CMSAdminImpersonationRequest cmsAdminImpersonationRequest);
}
Loading