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
2 changes: 1 addition & 1 deletion .github/workflows/cd-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ jobs:
run: |
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": \"✅ EC2 배포 성공! (브랜치: main)\"}" \
-d "{\"content\": \"✅ EC2 배포 성공!\"}" \
${{ secrets.DISCORD_WEBHOOK_URL }}

# ❌ 배포 실패 알림 (Discord)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/wayble/server/ServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
@EnableJpaAuditing
@EnableScheduling
@EnableElasticsearchRepositories(basePackages = "com.wayble.server.explore.repository")
@EnableElasticsearchRepositories(basePackages = {"com.wayble.server.explore.repository", "com.wayble.server.logging.repository"})
@EnableConfigurationProperties(TMapProperties.class)
@EntityScan(basePackages = "com.wayble.server")
public class ServerApplication {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.wayble.server.admin.controller;

import com.wayble.server.admin.dto.DailyStatsDto;
import com.wayble.server.admin.dto.SystemStatusDto;
import com.wayble.server.admin.service.AdminSystemService;
import com.wayble.server.admin.service.AdminUserService;
import com.wayble.server.admin.service.AdminWaybleZoneService;
import com.wayble.server.common.response.CommonResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -14,6 +16,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Slf4j
@Controller
Expand Down Expand Up @@ -74,13 +77,17 @@ public String adminDashboard(HttpSession session, Model model) {
adminSystemService.isFileStorageHealthy()
);

// 통계 데이터 조회
// 일일 통계 데이터 조회
DailyStatsDto dailyStats = adminSystemService.getDailyStats();

// 기존 통계 데이터 조회 (원래대로 복구)
long totalUserCount = adminUserService.getTotalUserCount();
long totalDeletedUserCount = adminUserService.getTotalDeletedUserCount();
long totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();

model.addAttribute("adminUsername", session.getAttribute("adminUsername"));
model.addAttribute("systemStatus", systemStatus);
model.addAttribute("dailyStats", dailyStats);
model.addAttribute("totalUserCount", totalUserCount);
model.addAttribute("totalDeletedUserCount", totalDeletedUserCount);
model.addAttribute("totalWaybleZoneCount", totalWaybleZoneCount);
Expand All @@ -93,4 +100,16 @@ public String adminLogout(HttpSession session) {
log.info("관리자 로그아웃");
return "redirect:/admin";
}

@GetMapping("/api/daily-stats")
@ResponseBody
public CommonResponse<DailyStatsDto> getDailyStats(HttpSession session) {
// 로그인 확인 (API용)
if (session.getAttribute("adminLoggedIn") == null) {
return CommonResponse.error(401, "관리자 인증이 필요합니다.");
}

DailyStatsDto dailyStats = adminSystemService.getDailyStats();
return CommonResponse.success(dailyStats);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/wayble/server/admin/dto/DailyStatsDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.wayble.server.admin.dto;

import lombok.Builder;

import java.time.LocalDate;

@Builder
public record DailyStatsDto(
LocalDate date,
long dailyRegistrationCount,
long dailyActiveUserCount,
long totalUserCount,
long totalWaybleZoneCount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import com.wayble.server.admin.dto.DailyStatsDto;
import com.wayble.server.logging.service.UserActionLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.sql.Connection;
import java.time.LocalDate;

@Slf4j
@Service
Expand All @@ -16,6 +19,9 @@ public class AdminSystemService {

private final ElasticsearchClient elasticsearchClient;
private final DataSource dataSource;
private final UserActionLogService userActionLogService;
private final AdminUserService adminUserService;
private final AdminWaybleZoneService adminWaybleZoneService;

public boolean isElasticsearchHealthy() {
try {
Expand Down Expand Up @@ -46,4 +52,42 @@ public boolean isFileStorageHealthy() {
// 파일 스토리지 상태 체크 (예: S3 연결 확인 등)
return true;
}

public DailyStatsDto getDailyStats() {
try {
LocalDate today = LocalDate.now();

// 일일 가입자 수
long dailyRegistrationCount = userActionLogService.getTodayUserRegistrationCount();

// 일일 활성 유저 수 (방문자)
long dailyActiveUserCount = userActionLogService.getTodayActiveUserCount();

// 전체 유저 수
long totalUserCount = adminUserService.getTotalUserCount();

// 전체 웨이블존 수
long totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();

return DailyStatsDto.builder()
.date(today)
.dailyRegistrationCount(dailyRegistrationCount)
.dailyActiveUserCount(dailyActiveUserCount)
.totalUserCount(totalUserCount)
.totalWaybleZoneCount(totalWaybleZoneCount)
.build();

} catch (Exception e) {
log.error("Failed to get daily stats", e);

// 에러 발생시 기본값 반환
return DailyStatsDto.builder()
.date(LocalDate.now())
.dailyRegistrationCount(0)
.dailyActiveUserCount(0)
.totalUserCount(0)
.totalWaybleZoneCount(0)
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ private AdminUserThumbnailDto convertToThumbnailDto(Object[] row) {
String email = (String) row[2];
LocalDate birthDate = row[3] != null ? ((Date) row[3]).toLocalDate() : null;
Gender gender = row[4] != null ? Gender.valueOf((String) row[4]) : null;
LoginType loginType = LoginType.valueOf((String) row[5]);
UserType userType = UserType.valueOf((String) row[6]);
LoginType loginType = row[5] != null ? LoginType.valueOf((String) row[5]) : null;
UserType userType = row[6] != null ? UserType.valueOf((String) row[6]) : null;
String disabilityType = (String) row[7];
String mobilityAid = (String) row[8];

Expand All @@ -181,8 +181,8 @@ private AdminUserDetailDto convertToDetailDto(Object[] row, long reviewCount, lo
String email = (String) row[3];
LocalDate birthDate = row[4] != null ? ((Date) row[4]).toLocalDate() : null;
Gender gender = row[5] != null ? Gender.valueOf((String) row[5]) : null;
LoginType loginType = LoginType.valueOf((String) row[6]);
UserType userType = UserType.valueOf((String) row[7]);
LoginType loginType = row[6] != null ? LoginType.valueOf((String) row[6]) : null;
UserType userType = row[7] != null ? UserType.valueOf((String) row[7]) : null;
String profileImageUrl = (String) row[8];
String disabilityType = (String) row[9];
String mobilityAid = (String) row[10];
Expand All @@ -203,8 +203,8 @@ private AdminUserDetailDto convertToDetailDtoWithStats(Object[] row, long review
String email = (String) row[3];
LocalDate birthDate = row[4] != null ? ((Date) row[4]).toLocalDate() : null;
Gender gender = row[5] != null ? Gender.valueOf((String) row[5]) : null;
LoginType loginType = LoginType.valueOf((String) row[6]);
UserType userType = UserType.valueOf((String) row[7]);
LoginType loginType = row[6] != null ? LoginType.valueOf((String) row[6]) : null;
UserType userType = row[7] != null ? UserType.valueOf((String) row[7]) : null;
String profileImageUrl = (String) row[8];
String disabilityType = (String) row[9];
String mobilityAid = (String) row[10];
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/wayble/server/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.wayble.server.common.config.security.jwt.JwtTokenProvider;
import com.wayble.server.common.exception.ApplicationException;
import com.wayble.server.logging.service.UserActionLogService;
import com.wayble.server.user.dto.UserLoginRequestDto;
import com.wayble.server.auth.dto.TokenResponseDto;
import com.wayble.server.auth.entity.RefreshToken;
Expand All @@ -22,6 +23,7 @@ public class AuthService {
private final RefreshTokenRepository refreshTokenRepository;
private final PasswordEncoder encoder;
private final JwtTokenProvider jwtProvider;
private final UserActionLogService userActionLogService;

public TokenResponseDto login(UserLoginRequestDto req) {
User user = userRepository.findByEmailAndLoginType(req.email(), req.loginType())
Expand All @@ -42,6 +44,12 @@ public TokenResponseDto login(UserLoginRequestDto req) {
.build();
refreshTokenRepository.save(entity);

// 첫 로그인시에도 활성 유저 로그 저장 (비동기, 하루 1회만)
userActionLogService.logTokenRefresh(
user.getId(),
user.getUserType() != null ? user.getUserType().name() : null
);

return new TokenResponseDto(accessToken, refreshToken);
}

Expand Down Expand Up @@ -73,6 +81,13 @@ public TokenResponseDto reissue(String refreshToken) {
refreshTokenRepository.save(saved);

String newAccessToken = jwtProvider.generateToken(userId, user.getUserType() != null ? user.getUserType().name() : null);

// 토큰 갱신 로그 저장 (비동기, 하루 1회만)
userActionLogService.logTokenRefresh(
userId,
user.getUserType() != null ? user.getUserType().name() : null
);

return new TokenResponseDto(newAccessToken, newRefreshToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wayble.server.common.config.security.jwt.JwtTokenProvider;
import com.wayble.server.common.exception.ApplicationException;
import com.wayble.server.logging.service.UserActionLogService;
import com.wayble.server.user.dto.KakaoLoginRequestDto;
import com.wayble.server.user.dto.KakaoLoginResponseDto;
import com.wayble.server.user.dto.KakaoUserInfoDto;
Expand All @@ -26,6 +27,7 @@ public class KakaoLoginService {
private final JwtTokenProvider jwtProvider;
private final ObjectMapper objectMapper;
private final WebClient webClient;
private final UserActionLogService userActionLogService;

private static final String KAKAO_USERINFO_URL = "https://kapi.kakao.com/v2/user/me";

Expand Down Expand Up @@ -66,6 +68,21 @@ public KakaoLoginResponseDto kakaoLogin(KakaoLoginRequestDto request) {
);
String refreshToken = jwtProvider.generateRefreshToken(user.getId());

// 로그 저장 (비동기)
if (isNewUser) {
// 신규 가입 로그
userActionLogService.logUserRegister(
user.getId(),
LoginType.KAKAO.name(),
user.getUserType() != null ? user.getUserType().name() : null
);
}

// 모든 카카오 로그인시 활성 유저 로그 저장 (하루 1회만)
userActionLogService.logTokenRefresh(
user.getId(),
user.getUserType() != null ? user.getUserType().name() : null
);

return KakaoLoginResponseDto.builder()
.accessToken(accessToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.wayble.server.logging.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class LoggingAsyncConfig {

@Bean("loggingTaskExecutor")
public Executor loggingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("logging-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/wayble/server/logging/entity/UserActionLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.wayble.server.logging.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "user_action_logs")
public class UserActionLog {

@Id
private String id;

@Field(type = FieldType.Long)
private Long userId;

@Field(type = FieldType.Keyword)
private String action;

@Field(type = FieldType.Text)
private String userAgent;

@Field(type = FieldType.Date)
private LocalDateTime timestamp;

@Field(type = FieldType.Keyword)
private String loginType;

@Field(type = FieldType.Keyword)
private String userType;

public enum ActionType {
USER_REGISTER,
USER_TOKEN_REFRESH
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.wayble.server.logging.repository;

import com.wayble.server.logging.entity.UserActionLog;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface UserActionLogRepository extends ElasticsearchRepository<UserActionLog, String> {

List<UserActionLog> findByActionAndTimestampBetween(String action, LocalDateTime start, LocalDateTime end);

List<UserActionLog> findByUserIdAndActionAndTimestampBetween(Long userId, String action, LocalDateTime start, LocalDateTime end);

long countByActionAndTimestampBetween(String action, LocalDateTime start, LocalDateTime end);

long countByUserIdAndActionAndTimestampBetween(Long userId, String action, LocalDateTime start, LocalDateTime end);

long countDistinctUserIdByActionAndTimestampBetween(String action, LocalDateTime start, LocalDateTime end);
}
Loading