Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.debatetimer.client.notifier;

import com.debatetimer.event.sharing.SharingFinishedEvent;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConsoleSharingNotifier implements SharingNotifier {

@Override
public void sendSharingFinished(SharingFinishedEvent event) {
log.info("공유 종료 정보가 채널로 발송되었습니다\n{}", SharingFinishedMessage.from(event));
}
}
17 changes: 2 additions & 15 deletions src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.debatetimer.client.notifier;

import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import io.micrometer.core.annotation.Timed;
import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;

@Slf4j
Expand All @@ -21,18 +18,9 @@ public class DiscordNotifier implements ErrorNotifier {
private final DiscordProperties properties;
private final JDA jda;

public DiscordNotifier(DiscordProperties discordProperties) {
public DiscordNotifier(DiscordProperties discordProperties, JDA jda) {
this.properties = discordProperties;
this.jda = initializeJda(properties.getToken());
}

private JDA initializeJda(String token) {
try {
return JDABuilder.createDefault(token).build().awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new DTInitializationException(InitializationErrorCode.JDA_INITIALIZATION_FAIL);
}
this.jda = jda;
}

@Timed(value = "discord.send_error_message")
Expand All @@ -56,4 +44,3 @@ private String getStackTraceAsString(Throwable throwable) {
.collect(Collectors.joining(DISCORD_LINE_SEPARATOR));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ public class DiscordProperties {

private final String token;
private final String channelId;
private final String sharingChannelId;

public DiscordProperties(String token, String channelId) {
public DiscordProperties(String token, String channelId, String sharingChannelId) {
validate(token);
validate(channelId);
validate(sharingChannelId);
this.token = token;
this.channelId = channelId;
this.sharingChannelId = sharingChannelId;
}

private void validate(String element) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.debatetimer.client.notifier;

import com.debatetimer.event.sharing.SharingFinishedEvent;
import io.micrometer.core.annotation.Timed;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;

public class DiscordSharingNotifier implements SharingNotifier {

private final DiscordProperties properties;
private final JDA jda;

public DiscordSharingNotifier(DiscordProperties properties, JDA jda) {
this.properties = properties;
this.jda = jda;
}

@Timed(value = "discord.send_sharing_finished")
@Override
public void sendSharingFinished(SharingFinishedEvent event) {
TextChannel channel = jda.getTextChannelById(properties.getSharingChannelId());
channel.sendMessage(SharingFinishedMessage.from(event)).queue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.debatetimer.client.notifier;

import com.debatetimer.event.sharing.SharingFinishedEvent;
import java.time.format.DateTimeFormatter;

final class SharingFinishedMessage {

private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");

// 공유 기록 id는 공유를 시작할 때마다 하나씩 늘어나므로 누적 공유 횟수로 보여준다
private static final String TEMPLATE = """
## 🎉 누적 **%,d**번째 라이브 공유 종료!
> 👤 **사회자** `%d` · %s
> 📋 **테이블** `%d` · %s
> ⏱️ **공유 시간** **%d분 %d초**
> 🕐 **진행** %s → %s
> 👥 **참여 청중** **%d명**""";

private SharingFinishedMessage() {
}

static String from(SharingFinishedEvent event) {
return TEMPLATE.formatted(
event.sharingLogId(),
event.memberId(),
event.memberEmail(),
event.tableId(),
event.tableName(),
event.durationSeconds() / 60,
event.durationSeconds() % 60,
event.startedAt().format(DATE_TIME_FORMATTER),
event.endedAt().format(TIME_FORMATTER),
event.audienceCount()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.debatetimer.client.notifier;

import com.debatetimer.event.sharing.SharingFinishedEvent;

public interface SharingNotifier {

void sendSharingFinished(SharingFinishedEvent event);
}
14 changes: 14 additions & 0 deletions src/main/java/com/debatetimer/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.debatetimer.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

/**
* 비동기 작업은 Spring Boot가 구성하는 기본 TaskExecutor(applicationTaskExecutor)에서 실행한다.
* Executor 빈을 따로 등록하면 기본 TaskExecutor 구성이 빠지므로 직접 등록하지 않는다.
*/
@Configuration
@EnableAsync
public class AsyncConfig {

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.debatetimer.config;

import com.debatetimer.client.oauth.OAuthProperties;
import com.debatetimer.controller.admin.AdminProperties;
import com.debatetimer.controller.tool.jwt.JwtTokenProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties({OAuthProperties.class, JwtTokenProperties.class})
@EnableConfigurationProperties({OAuthProperties.class, JwtTokenProperties.class, AdminProperties.class})
public class AuthenticationConfig {

}
31 changes: 29 additions & 2 deletions src/main/java/com/debatetimer/config/NotifierConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@


import com.debatetimer.client.notifier.ConsoleNotifier;
import com.debatetimer.client.notifier.ConsoleSharingNotifier;
import com.debatetimer.client.notifier.DiscordNotifier;
import com.debatetimer.client.notifier.DiscordProperties;
import com.debatetimer.client.notifier.DiscordSharingNotifier;
import com.debatetimer.client.notifier.ErrorNotifier;
import com.debatetimer.client.notifier.SharingNotifier;
import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -23,9 +30,24 @@ public static class DiscordNotifierConfig {

private final DiscordProperties discordProperties;

@Bean(destroyMethod = "shutdown")
public JDA jda() {
try {
return JDABuilder.createDefault(discordProperties.getToken()).build().awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new DTInitializationException(InitializationErrorCode.JDA_INITIALIZATION_FAIL);
}
}

@Bean
public ErrorNotifier discordNotifier(JDA jda) {
return new DiscordNotifier(discordProperties, jda);
}

@Bean
public ErrorNotifier discordNotifier() {
return new DiscordNotifier(discordProperties);
public SharingNotifier discordSharingNotifier(JDA jda) {
return new DiscordSharingNotifier(discordProperties, jda);
}
}

Expand All @@ -37,5 +59,10 @@ public static class ConsoleNotifierConfig {
public ErrorNotifier consoleNotifier() {
return new ConsoleNotifier();
}

@Bean
public SharingNotifier consoleSharingNotifier() {
return new ConsoleSharingNotifier();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.debatetimer.controller.admin;

import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import jakarta.annotation.Nullable;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
* 어드민 비밀번호를 확인한다. 비교 시간으로 비밀번호를 추측할 수 없도록 고정 시간 비교를 쓴다.
*/
@Component
@RequiredArgsConstructor
public class AdminAuthorizer {

private final AdminProperties adminProperties;

public void authorize(@Nullable String password) {
if (password == null || !MessageDigest.isEqual(
password.getBytes(StandardCharsets.UTF_8),
adminProperties.getPassword().getBytes(StandardCharsets.UTF_8))) {
throw new DTClientErrorException(ClientErrorCode.INVALID_ADMIN_PASSWORD);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.debatetimer.controller.admin;

import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@ConfigurationProperties(prefix = "admin")
public class AdminProperties {

private final String password;

public AdminProperties(String password) {
if (password == null || password.isBlank()) {
throw new DTInitializationException(InitializationErrorCode.ADMIN_PASSWORD_EMPTY);
}
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.debatetimer.controller.admin;

import com.debatetimer.dto.admin.SharingStatsResponse;
import com.debatetimer.service.admin.SharingStatsService;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AdminSharingController {

public static final String ADMIN_PASSWORD_HEADER = "X-Admin-Password";

private final AdminAuthorizer adminAuthorizer;
private final SharingStatsService sharingStatsService;

@GetMapping("/api/admin/sharing/stats")
@ResponseStatus(HttpStatus.OK)
public SharingStatsResponse getSharingStats(
@RequestHeader(name = ADMIN_PASSWORD_HEADER, required = false) String password,
@RequestParam("from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
@RequestParam("to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to
) {
adminAuthorizer.authorize(password);
return sharingStatsService.getDailyStats(from, to);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.debatetimer.domain.sharing;

public enum SharingLogStatus {

SHARING,
FINISHED,
ABANDONED,
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public long getTotalTimeBoxTimes(long tableId) {
return timeBoxRepository.sumTimeByTableId(tableId);
}

@Transactional(readOnly = true)
public int countTimeBoxes(long tableId) {
return timeBoxRepository.countByCustomizeTableId(tableId);
}

private List<CustomizeTimeBox> toCustomizeTimeBoxes(
List<CustomizeTimeBoxEntity> timeBoxEntities,
List<BellEntity> bellEntities
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.debatetimer.dto.admin;

import java.time.LocalDate;

public record SharingDailyStatResponse(
LocalDate date,
int finishedCount,
double averageMinutes
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/debatetimer/dto/admin/SharingStatsResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.debatetimer.dto.admin;

import java.time.LocalDate;
import java.util.List;

public record SharingStatsResponse(
LocalDate from,
LocalDate to,
List<SharingDailyStatResponse> days
) {
}
Loading
Loading