diff --git a/api/cached_client.go b/api/cached_client.go index 5cb7c09..0f19716 100644 --- a/api/cached_client.go +++ b/api/cached_client.go @@ -51,124 +51,124 @@ func NewCachedSolvedACClient() *CachedSolvedACClient { } // Close는 캐시 정리 워커를 중지시킵니다. -func (c *CachedSolvedACClient) Close() { - if c.cleanupCancel != nil { - c.cleanupCancel() +func (cachedClient *CachedSolvedACClient) Close() { + if cachedClient.cleanupCancel != nil { + cachedClient.cleanupCancel() utils.Info("Cache cleanup worker stopped.") } } // GetUserInfo 캐시를 통해 사용자 정보를 조회합니다 -func (c *CachedSolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) { - atomic.AddInt64(&c.totalCalls, 1) +func (cachedClient *CachedSolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) { + atomic.AddInt64(&cachedClient.totalCalls, 1) // 캐시에서 먼저 조회 - if cachedData, found := c.cache.GetUserInfo(handle); found { - atomic.AddInt64(&c.cacheHits, 1) + if cachedData, found := cachedClient.cache.GetUserInfo(handle); found { + atomic.AddInt64(&cachedClient.cacheHits, 1) utils.Debug("Cache hit for user info: %s", handle) return cachedData.(*UserInfo), nil } // 캐시 미스 - API 호출 - atomic.AddInt64(&c.cacheMisses, 1) + atomic.AddInt64(&cachedClient.cacheMisses, 1) utils.Debug("Cache miss for user info: %s, calling API", handle) - userInfo, err := c.client.GetUserInfo(ctx, handle) + userInfo, err := cachedClient.client.GetUserInfo(ctx, handle) if err != nil { return nil, err } // 성공한 응답을 캐시에 저장 - c.cache.SetUserInfo(handle, userInfo) + cachedClient.cache.SetUserInfo(handle, userInfo) return userInfo, nil } // GetUserTop100 캐시를 통해 사용자 TOP 100을 조회합니다 -func (c *CachedSolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) { - atomic.AddInt64(&c.totalCalls, 1) +func (cachedClient *CachedSolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) { + atomic.AddInt64(&cachedClient.totalCalls, 1) // 캐시에서 먼저 조회 - if cachedData, found := c.cache.GetUserTop100(handle); found { - atomic.AddInt64(&c.cacheHits, 1) + if cachedData, found := cachedClient.cache.GetUserTop100(handle); found { + atomic.AddInt64(&cachedClient.cacheHits, 1) utils.Debug("Cache hit for user top100: %s", handle) return cachedData.(*Top100Response), nil } // 캐시 미스 - API 호출 - atomic.AddInt64(&c.cacheMisses, 1) + atomic.AddInt64(&cachedClient.cacheMisses, 1) utils.Debug("Cache miss for user top100: %s, calling API", handle) - top100, err := c.client.GetUserTop100(ctx, handle) + top100, err := cachedClient.client.GetUserTop100(ctx, handle) if err != nil { return nil, err } // 성공한 응답을 캐시에 저장 - c.cache.SetUserTop100(handle, top100) + cachedClient.cache.SetUserTop100(handle, top100) return top100, nil } // GetUserAdditionalInfo 캐시를 통해 사용자 추가 정보를 조회합니다 -func (c *CachedSolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) { - atomic.AddInt64(&c.totalCalls, 1) +func (cachedClient *CachedSolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) { + atomic.AddInt64(&cachedClient.totalCalls, 1) // 캐시에서 먼저 조회 - if cachedData, found := c.cache.GetUserAdditionalInfo(handle); found { - atomic.AddInt64(&c.cacheHits, 1) + if cachedData, found := cachedClient.cache.GetUserAdditionalInfo(handle); found { + atomic.AddInt64(&cachedClient.cacheHits, 1) utils.Debug("Cache hit for user additional info: %s", handle) return cachedData.(*UserAdditionalInfo), nil } // 캐시 미스 - API 호출 - atomic.AddInt64(&c.cacheMisses, 1) + atomic.AddInt64(&cachedClient.cacheMisses, 1) utils.Debug("Cache miss for user additional info: %s, calling API", handle) - additionalInfo, err := c.client.GetUserAdditionalInfo(ctx, handle) + additionalInfo, err := cachedClient.client.GetUserAdditionalInfo(ctx, handle) if err != nil { return nil, err } // 성공한 응답을 캐시에 저장 - c.cache.SetUserAdditionalInfo(handle, additionalInfo) + cachedClient.cache.SetUserAdditionalInfo(handle, additionalInfo) return additionalInfo, nil } // GetUserOrganizations 지정된 사용자의 소속 조직 목록을 가져옵니다 (캐시 포함) -func (c *CachedSolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) { - atomic.AddInt64(&c.totalCalls, 1) +func (cachedClient *CachedSolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) { + atomic.AddInt64(&cachedClient.totalCalls, 1) // 캐시에서 먼저 조회 - if cachedData, found := c.cache.GetUserOrganizations(handle); found { - atomic.AddInt64(&c.cacheHits, 1) + if cachedData, found := cachedClient.cache.GetUserOrganizations(handle); found { + atomic.AddInt64(&cachedClient.cacheHits, 1) utils.Debug("Cache hit for user organizations: %s", handle) return cachedData.([]Organization), nil } // 캐시 미스 - API 호출 - atomic.AddInt64(&c.cacheMisses, 1) + atomic.AddInt64(&cachedClient.cacheMisses, 1) utils.Debug("Cache miss for user organizations: %s, calling API", handle) - organizations, err := c.client.GetUserOrganizations(ctx, handle) + organizations, err := cachedClient.client.GetUserOrganizations(ctx, handle) if err != nil { return nil, err } // 성공한 응답을 캐시에 저장 - c.cache.SetUserOrganizations(handle, organizations) + cachedClient.cache.SetUserOrganizations(handle, organizations) return organizations, nil } // GetCacheStats 캐시 통계를 반환합니다 -func (c *CachedSolvedACClient) GetCacheStats() CacheMetrics { - cacheStats := c.cache.GetStats() +func (cachedClient *CachedSolvedACClient) GetCacheStats() CacheMetrics { + cacheStats := cachedClient.cache.GetStats() - totalCalls := atomic.LoadInt64(&c.totalCalls) - hits := atomic.LoadInt64(&c.cacheHits) - misses := atomic.LoadInt64(&c.cacheMisses) + totalCalls := atomic.LoadInt64(&cachedClient.totalCalls) + hits := atomic.LoadInt64(&cachedClient.cacheHits) + misses := atomic.LoadInt64(&cachedClient.cacheMisses) var hitRate float64 if totalCalls > 0 { @@ -198,38 +198,38 @@ type CacheMetrics struct { } // String CacheMetrics의 문자열 표현을 반환합니다 -func (m CacheMetrics) String() string { +func (metrics CacheMetrics) String() string { return fmt.Sprintf("API Cache Stats: Calls=%d, Hits=%d, Misses=%d, Hit Rate=%.2f%%, Cached Items: UserInfo=%d, Top100=%d, Additional=%d", - m.TotalCalls, m.CacheHits, m.CacheMisses, m.HitRate, - m.UserInfoCached, m.UserTop100Cached, m.UserAdditionalCached) + metrics.TotalCalls, metrics.CacheHits, metrics.CacheMisses, metrics.HitRate, + metrics.UserInfoCached, metrics.UserTop100Cached, metrics.UserAdditionalCached) } // ClearCache 모든 캐시를 삭제합니다 -func (c *CachedSolvedACClient) ClearCache() { - c.cache.Clear() - atomic.StoreInt64(&c.cacheHits, 0) - atomic.StoreInt64(&c.cacheMisses, 0) - atomic.StoreInt64(&c.totalCalls, 0) +func (cachedClient *CachedSolvedACClient) ClearCache() { + cachedClient.cache.Clear() + atomic.StoreInt64(&cachedClient.cacheHits, 0) + atomic.StoreInt64(&cachedClient.cacheMisses, 0) + atomic.StoreInt64(&cachedClient.totalCalls, 0) utils.Info("API cache cleared") } // WarmupCache 주요 참가자들에 대한 캐시를 미리 로드합니다 -func (c *CachedSolvedACClient) WarmupCache(handles []string) error { +func (cachedClient *CachedSolvedACClient) WarmupCache(handles []string) error { utils.Info("Starting cache warmup for %d users", len(handles)) for _, handle := range handles { // 이미 캐시에 있다면 스킵 - if _, found := c.cache.GetUserInfo(handle); found { + if _, found := cachedClient.cache.GetUserInfo(handle); found { continue } // 백그라운드에서 데이터 로드 go func(h string) { ctx := context.Background() - if _, err := c.GetUserInfo(ctx, h); err != nil { + if _, err := cachedClient.GetUserInfo(ctx, h); err != nil { utils.Warn("Cache warmup failed for user info %s: %v", h, err) } - if _, err := c.GetUserTop100(ctx, h); err != nil { + if _, err := cachedClient.GetUserTop100(ctx, h); err != nil { utils.Warn("Cache warmup failed for top100 %s: %v", h, err) } }(handle) diff --git a/api/solvedac.go b/api/solvedac.go index dde2922..6dd4132 100644 --- a/api/solvedac.go +++ b/api/solvedac.go @@ -83,17 +83,17 @@ func NewSolvedACClient() *SolvedACClient { } // GetUserInfo 지정된 핸들의 사용자 정보를 가져옵니다 -func (c *SolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) { +func (client *SolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) { if !utils.IsValidBaekjoonID(handle) { return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle) } - url := fmt.Sprintf("%s/user/show?handle=%s", c.baseURL, handle) - return c.getUserInfoWithRetry(ctx, url, handle) + url := fmt.Sprintf("%s/user/show?handle=%s", client.baseURL, handle) + return client.getUserInfoWithRetry(ctx, url, handle) } // doRequest 공통 HTTP 요청 및 재시도 로직 -func (c *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle string) ([]byte, error) { +func (client *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle string) ([]byte, error) { var lastErr error for attempt := 0; attempt < constants.MaxRetries; attempt++ { @@ -110,7 +110,7 @@ func (c *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle continue } - resp, err := c.client.Do(req) + resp, err := client.client.Do(req) if err != nil { lastErr = fmt.Errorf("%s 조회 실패: %w", requestType, err) utils.Warn("Attempt %d failed for %s %s: %v", attempt+1, requestType, handle, err) @@ -149,8 +149,8 @@ func (c *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle } // 재시도 로직을 포함한 사용자 정보 조회 -func (c *SolvedACClient) getUserInfoWithRetry(ctx context.Context, url, handle string) (*UserInfo, error) { - body, err := c.doRequest(ctx, url, "user info", handle) +func (client *SolvedACClient) getUserInfoWithRetry(ctx context.Context, url, handle string) (*UserInfo, error) { + body, err := client.doRequest(ctx, url, "user info", handle) if err != nil { return nil, err } @@ -167,18 +167,18 @@ func (c *SolvedACClient) getUserInfoWithRetry(ctx context.Context, url, handle s } // GetUserTop100 지정된 사용자의 TOP 100 문제를 가져옵니다 -func (c *SolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) { +func (client *SolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) { if !utils.IsValidBaekjoonID(handle) { return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle) } - url := fmt.Sprintf("%s/user/top_100?handle=%s", c.baseURL, handle) - return c.getUserTop100WithRetry(ctx, url, handle) + url := fmt.Sprintf("%s/user/top_100?handle=%s", client.baseURL, handle) + return client.getUserTop100WithRetry(ctx, url, handle) } // 재시도 로직을 포함한 TOP 100 조회 -func (c *SolvedACClient) getUserTop100WithRetry(ctx context.Context, url, handle string) (*Top100Response, error) { - body, err := c.doRequest(ctx, url, "top 100", handle) +func (client *SolvedACClient) getUserTop100WithRetry(ctx context.Context, url, handle string) (*Top100Response, error) { + body, err := client.doRequest(ctx, url, "top 100", handle) if err != nil { return nil, err } @@ -194,18 +194,18 @@ func (c *SolvedACClient) getUserTop100WithRetry(ctx context.Context, url, handle } // GetUserAdditionalInfo 지정된 사용자의 추가 정보를 가져옵니다 -func (c *SolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) { +func (client *SolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) { if !utils.IsValidBaekjoonID(handle) { return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle) } - url := fmt.Sprintf("%s/user/additional_info?handle=%s", c.baseURL, handle) - return c.getUserAdditionalInfoWithRetry(ctx, url, handle) + url := fmt.Sprintf("%s/user/additional_info?handle=%s", client.baseURL, handle) + return client.getUserAdditionalInfoWithRetry(ctx, url, handle) } // 재시도 로직을 포함한 사용자 추가 정보 조회 -func (c *SolvedACClient) getUserAdditionalInfoWithRetry(ctx context.Context, url, handle string) (*UserAdditionalInfo, error) { - body, err := c.doRequest(ctx, url, "additional info", handle) +func (client *SolvedACClient) getUserAdditionalInfoWithRetry(ctx context.Context, url, handle string) (*UserAdditionalInfo, error) { + body, err := client.doRequest(ctx, url, "additional info", handle) if err != nil { return nil, err } @@ -229,18 +229,18 @@ func (c *SolvedACClient) getUserAdditionalInfoWithRetry(ctx context.Context, url } // GetUserOrganizations 지정된 사용자의 소속 조직 목록을 가져옵니다 -func (c *SolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) { +func (client *SolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) { if !utils.IsValidBaekjoonID(handle) { return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle) } - url := fmt.Sprintf("%s/user/organizations?handle=%s", c.baseURL, handle) - return c.getUserOrganizationsWithRetry(ctx, url, handle) + url := fmt.Sprintf("%s/user/organizations?handle=%s", client.baseURL, handle) + return client.getUserOrganizationsWithRetry(ctx, url, handle) } // 재시도 로직을 포함한 사용자 조직 목록 조회 -func (c *SolvedACClient) getUserOrganizationsWithRetry(ctx context.Context, url, handle string) ([]Organization, error) { - body, err := c.doRequest(ctx, url, "user organizations", handle) +func (client *SolvedACClient) getUserOrganizationsWithRetry(ctx context.Context, url, handle string) ([]Organization, error) { + body, err := client.doRequest(ctx, url, "user organizations", handle) if err != nil { return nil, err } diff --git a/bot/commands.go b/bot/commands.go index 8cd858c..320eb01 100644 --- a/bot/commands.go +++ b/bot/commands.go @@ -20,45 +20,45 @@ type CommandHandler struct { } func NewCommandHandler(deps *CommandDependencies) *CommandHandler { - ch := &CommandHandler{ + handler := &CommandHandler{ deps: deps, } - ch.competitionHandler = NewCompetitionHandler(ch) - return ch + handler.competitionHandler = NewCompetitionHandler(handler) + return handler } // HandleMessage Discord 메시지를 처리합니다 -func (ch *CommandHandler) HandleMessage(s *discordgo.Session, m *discordgo.MessageCreate) { - if ch.shouldIgnoreMessage(s, m) { +func (handler *CommandHandler) HandleMessage(session *discordgo.Session, message *discordgo.MessageCreate) { + if handler.shouldIgnoreMessage(session, message) { return } - command, params, isDM := ch.parseMessage(m) + command, params, isDM := handler.parseMessage(message) if command == "" { return } - ch.routeCommand(s, m, command, params, isDM) + handler.routeCommand(session, message, command, params, isDM) } // shouldIgnoreMessage 메시지를 무시해야 하는지 확인합니다 -func (ch *CommandHandler) shouldIgnoreMessage(s *discordgo.Session, m *discordgo.MessageCreate) bool { +func (handler *CommandHandler) shouldIgnoreMessage(session *discordgo.Session, message *discordgo.MessageCreate) bool { // 봇 자신의 메시지는 무시 - if m.Author.ID == s.State.User.ID { + if message.Author.ID == session.State.User.ID { return true } // DM 디버깅 로그 - if m.GuildID == "" { - utils.Debug("DM received from %s", m.Author.Username) + if message.GuildID == "" { + utils.Debug("DM received from %s", message.Author.Username) } return false } // parseMessage 메시지를 파싱하여 명령어와 매개변수를 추출합니다 -func (ch *CommandHandler) parseMessage(m *discordgo.MessageCreate) (command string, params []string, isDM bool) { - content := strings.TrimSpace(m.Content) +func (handler *CommandHandler) parseMessage(message *discordgo.MessageCreate) (command string, params []string, isDM bool) { + content := strings.TrimSpace(message.Content) if !strings.HasPrefix(content, constants.CommandPrefix) { return "", nil, false } @@ -70,100 +70,100 @@ func (ch *CommandHandler) parseMessage(m *discordgo.MessageCreate) (command stri command = args[0][constants.CommandPrefixLength:] params = args[1:] - isDM = m.GuildID == "" + isDM = message.GuildID == "" return command, params, isDM } // routeCommand 명령어를 해당 핸들러로 라우팅합니다 -func (ch *CommandHandler) routeCommand(s *discordgo.Session, m *discordgo.MessageCreate, command string, params []string, isDM bool) { +func (handler *CommandHandler) routeCommand(session *discordgo.Session, message *discordgo.MessageCreate, command string, params []string, isDM bool) { // 명령어 사용 텔레메트리 전송 - isAdmin := ch.isAdmin(s, m) - if ch.deps.MetricsClient != nil { - ch.deps.MetricsClient.SendCommandMetric(command, isAdmin) + isAdmin := handler.isAdmin(session, message) + if handler.deps.MetricsClient != nil { + handler.deps.MetricsClient.SendCommandMetric(command, isAdmin) } switch command { case "help", "도움말": - ch.handleHelp(s, m) + handler.handleHelp(session, message) case "register", "등록": - ch.handleRegister(s, m, params) + handler.handleRegister(session, message, params) case "scoreboard", "스코어보드": - ch.handleScoreboardCommand(s, m, isDM) + handler.handleScoreboardCommand(session, message, isDM) case "competition", "대회": - ch.competitionHandler.HandleCompetition(s, m, params) + handler.competitionHandler.HandleCompetition(session, message, params) case "participants", "참가자": - ch.handleParticipants(s, m) + handler.handleParticipants(session, message) case "remove", "삭제": - ch.handleRemoveParticipant(s, m, params) + handler.handleRemoveParticipant(session, message, params) case "cache", "캐시": - ch.handleCacheStats(s, m) + handler.handleCacheStats(session, message) case "ping": - ch.handlePing(s, m) + handler.handlePing(session, message) } } // handleScoreboardCommand 스코어보드 명령어를 처리합니다 (DM 체크 포함) -func (ch *CommandHandler) handleScoreboardCommand(s *discordgo.Session, m *discordgo.MessageCreate, isDM bool) { +func (handler *CommandHandler) handleScoreboardCommand(session *discordgo.Session, message *discordgo.MessageCreate, isDM bool) { if isDM { - if _, err := s.ChannelMessageSend(m.ChannelID, constants.MsgScoreboardDMOnly); err != nil { + if _, err := session.ChannelMessageSend(message.ChannelID, constants.MsgScoreboardDMOnly); err != nil { utils.Error("Failed to send DM response: %v", err) } return } - ch.handleScoreboard(s, m) + handler.handleScoreboard(session, message) } // handlePing ping 명령어를 처리합니다 -func (ch *CommandHandler) handlePing(s *discordgo.Session, m *discordgo.MessageCreate) { - if err := errors.SendDiscordInfo(s, m.ChannelID, constants.MsgPong); err != nil { +func (handler *CommandHandler) handlePing(session *discordgo.Session, message *discordgo.MessageCreate) { + if err := errors.SendDiscordInfo(session, message.ChannelID, constants.MsgPong); err != nil { utils.Error("Failed to send ping response: %v", err) } } -func (ch *CommandHandler) handleHelp(s *discordgo.Session, m *discordgo.MessageCreate) { - if _, err := s.ChannelMessageSend(m.ChannelID, constants.HelpMessage); err != nil { +func (handler *CommandHandler) handleHelp(session *discordgo.Session, message *discordgo.MessageCreate) { + if _, err := session.ChannelMessageSend(message.ChannelID, constants.HelpMessage); err != nil { utils.Error("DISCORD API ERROR: Failed to send help message: %v", err) } } -func (ch *CommandHandler) handleRegister(s *discordgo.Session, m *discordgo.MessageCreate, params []string) { - errorHandlers := utils.NewErrorHandlerFactory(s, m.ChannelID) +func (handler *CommandHandler) handleRegister(session *discordgo.Session, message *discordgo.MessageCreate, params []string) { + errorHandlers := utils.NewErrorHandlerFactory(session, message.ChannelID) // 1. 기본 매개변수 검증 - name, baekjoonID, ok := ch.validateRegisterParams(params, errorHandlers) + name, baekjoonID, ok := handler.validateRegisterParams(params, errorHandlers) if !ok { return } // 2. 대회 상태 확인 - if !ch.validateCompetitionStatus(errorHandlers) { + if !handler.validateCompetitionStatus(errorHandlers) { return } // 3. solved.ac 사용자 정보 조회 및 검증 - userInfo, ok := ch.validateSolvedACUser(name, baekjoonID, errorHandlers) + userInfo, ok := handler.validateSolvedACUser(name, baekjoonID, errorHandlers) if !ok { return } // 4. 숭실대학교 소속 검증 - organizationID, ok := ch.validateUniversityAffiliation(baekjoonID, errorHandlers) + organizationID, ok := handler.validateUniversityAffiliation(baekjoonID, errorHandlers) if !ok { return } // 5. 참가자 등록 - if !ch.registerParticipant(name, baekjoonID, userInfo, organizationID, errorHandlers) { + if !handler.registerParticipant(name, baekjoonID, userInfo, organizationID, errorHandlers) { return } // 6. 성공 메시지 전송 - ch.sendRegistrationSuccess(s, m.ChannelID, name, userInfo) + handler.sendRegistrationSuccess(session, message.ChannelID, name, userInfo) } // validateRegisterParams 등록 매개변수를 검증합니다 -func (ch *CommandHandler) validateRegisterParams(params []string, errorHandlers *utils.ErrorHandlerFactory) (name, baekjoonID string, ok bool) { +func (handler *CommandHandler) validateRegisterParams(params []string, errorHandlers *utils.ErrorHandlerFactory) (name, baekjoonID string, ok bool) { if len(params) < 2 { errorHandlers.Validation().HandleInvalidParams("REGISTER_INVALID_PARAMS", "Invalid register parameters", @@ -174,8 +174,8 @@ func (ch *CommandHandler) validateRegisterParams(params []string, errorHandlers } // validateCompetitionStatus 대회 상태를 확인합니다 -func (ch *CommandHandler) validateCompetitionStatus(errorHandlers *utils.ErrorHandlerFactory) bool { - competition := ch.deps.Storage.GetCompetition() +func (handler *CommandHandler) validateCompetitionStatus(errorHandlers *utils.ErrorHandlerFactory) bool { + competition := handler.deps.Storage.GetCompetition() if competition == nil { errorHandlers.Data().HandleNoActiveCompetition() return false @@ -193,24 +193,24 @@ func (ch *CommandHandler) validateCompetitionStatus(errorHandlers *utils.ErrorHa } // validateSolvedACUser solved.ac 사용자 정보를 조회하고 이름을 검증합니다 -func (ch *CommandHandler) validateSolvedACUser(name, baekjoonID string, errorHandlers *utils.ErrorHandlerFactory) (userInfo interface{}, ok bool) { +func (handler *CommandHandler) validateSolvedACUser(name, baekjoonID string, errorHandlers *utils.ErrorHandlerFactory) (userInfo interface{}, ok bool) { // solved.ac 사용자 정보 조회 ctx := context.Background() - info, err := ch.deps.APIClient.GetUserInfo(ctx, baekjoonID) + info, err := handler.deps.APIClient.GetUserInfo(ctx, baekjoonID) if err != nil { errorHandlers.API().HandleBaekjoonUserNotFound(baekjoonID, err) return nil, false } // solved.ac 추가 정보 조회 (본명 확인용) - additionalInfo, err := ch.deps.APIClient.GetUserAdditionalInfo(ctx, baekjoonID) + additionalInfo, err := handler.deps.APIClient.GetUserAdditionalInfo(ctx, baekjoonID) if err != nil { errorHandlers.API().HandleBaekjoonUserNotFound(baekjoonID, err) return nil, false } // solved.ac에 등록된 이름 추출 및 검증 - solvedacName := ch.extractSolvedACName(additionalInfo, errorHandlers) + solvedacName := handler.extractSolvedACName(additionalInfo, errorHandlers) if solvedacName == "" { return nil, false } @@ -224,8 +224,8 @@ func (ch *CommandHandler) validateSolvedACUser(name, baekjoonID string, errorHan } // 스프레드시트에서 이름 검증 - if ch.deps.SheetsClient != nil { - isInList, err := ch.deps.SheetsClient.IsNameInParticipantList(name) + if handler.deps.SheetsClient != nil { + isInList, err := handler.deps.SheetsClient.IsNameInParticipantList(name) if err != nil { utils.Warn("Failed to check participant list: %v", err) errorHandlers.System().HandleSystemError("SHEETS_CHECK_FAILED", @@ -262,7 +262,7 @@ func (ch *CommandHandler) validateSolvedACUser(name, baekjoonID string, errorHan } // assertUserAdditionalInfo performs type assertion for UserAdditionalInfo with error handling -func (ch *CommandHandler) assertUserAdditionalInfo(additionalInfo interface{}, errorHandlers *utils.ErrorHandlerFactory) (*api.UserAdditionalInfo, bool) { +func (handler *CommandHandler) assertUserAdditionalInfo(additionalInfo interface{}, errorHandlers *utils.ErrorHandlerFactory) (*api.UserAdditionalInfo, bool) { info, ok := additionalInfo.(*api.UserAdditionalInfo) if !ok { errorHandlers.System().HandleSystemError("TYPE_ASSERTION_FAILED", "Failed to process user additional info", "내부 처리 오류가 발생했습니다.", nil) @@ -272,7 +272,7 @@ func (ch *CommandHandler) assertUserAdditionalInfo(additionalInfo interface{}, e } // assertUserInfo performs type assertion for UserInfo with error handling -func (ch *CommandHandler) assertUserInfo(userInfo interface{}, errorHandlers *utils.ErrorHandlerFactory) (*api.UserInfo, bool) { +func (handler *CommandHandler) assertUserInfo(userInfo interface{}, errorHandlers *utils.ErrorHandlerFactory) (*api.UserInfo, bool) { info, ok := userInfo.(*api.UserInfo) if !ok { errorHandlers.System().HandleSystemError("TYPE_ASSERTION_FAILED", "Failed to process user info", "내부 처리 오류가 발생했습니다.", nil) @@ -282,8 +282,8 @@ func (ch *CommandHandler) assertUserInfo(userInfo interface{}, errorHandlers *ut } // extractSolvedACName solved.ac 추가 정보에서 이름을 추출합니다 -func (ch *CommandHandler) extractSolvedACName(additionalInfo interface{}, errorHandlers *utils.ErrorHandlerFactory) string { - info, ok := ch.assertUserAdditionalInfo(additionalInfo, errorHandlers) +func (handler *CommandHandler) extractSolvedACName(additionalInfo interface{}, errorHandlers *utils.ErrorHandlerFactory) string { + info, ok := handler.assertUserAdditionalInfo(additionalInfo, errorHandlers) if !ok { return "" } @@ -301,10 +301,10 @@ func (ch *CommandHandler) extractSolvedACName(additionalInfo interface{}, errorH } // validateUniversityAffiliation 사용자의 학교 소속을 검증합니다 -func (ch *CommandHandler) validateUniversityAffiliation(baekjoonID string, errorHandlers *utils.ErrorHandlerFactory) (organizationID int, ok bool) { +func (handler *CommandHandler) validateUniversityAffiliation(baekjoonID string, errorHandlers *utils.ErrorHandlerFactory) (organizationID int, ok bool) { // solved.ac에서 사용자의 조직 정보 조회 ctx := context.Background() - organizations, err := ch.deps.APIClient.GetUserOrganizations(ctx, baekjoonID) + organizations, err := handler.deps.APIClient.GetUserOrganizations(ctx, baekjoonID) if err != nil { errorHandlers.API().HandleBaekjoonUserNotFound(baekjoonID, err) return 0, false @@ -325,75 +325,75 @@ func (ch *CommandHandler) validateUniversityAffiliation(baekjoonID string, error } // registerParticipant 참가자를 등록합니다 -func (ch *CommandHandler) registerParticipant(name, baekjoonID string, userInfo interface{}, organizationID int, errorHandlers *utils.ErrorHandlerFactory) bool { - info, ok := ch.assertUserInfo(userInfo, errorHandlers) +func (handler *CommandHandler) registerParticipant(name, baekjoonID string, userInfo interface{}, organizationID int, errorHandlers *utils.ErrorHandlerFactory) bool { + info, ok := handler.assertUserInfo(userInfo, errorHandlers) if !ok { return false } - err := ch.deps.Storage.AddParticipant(name, baekjoonID, info.Tier, info.Rating, organizationID) + err := handler.deps.Storage.AddParticipant(name, baekjoonID, info.Tier, info.Rating, organizationID) if err != nil { errorHandlers.Data().HandleParticipantAlreadyExists(baekjoonID) return false } // 참가자 등록 텔레메트리 전송 - if ch.deps.MetricsClient != nil { - participantCount := len(ch.deps.Storage.GetParticipants()) - ch.deps.MetricsClient.SendCompetitionMetric("participant_registered", participantCount) + if handler.deps.MetricsClient != nil { + participantCount := len(handler.deps.Storage.GetParticipants()) + handler.deps.MetricsClient.SendCompetitionMetric("participant_registered", participantCount) } return true } // sendRegistrationSuccess 등록 성공 메시지를 전송합니다 -func (ch *CommandHandler) sendRegistrationSuccess(s *discordgo.Session, channelID, name string, userInfo interface{}) { - errorHandlers := utils.NewErrorHandlerFactory(s, channelID) - info, ok := ch.assertUserInfo(userInfo, errorHandlers) +func (handler *CommandHandler) sendRegistrationSuccess(session *discordgo.Session, channelID, name string, userInfo interface{}) { + errorHandlers := utils.NewErrorHandlerFactory(session, channelID) + info, ok := handler.assertUserInfo(userInfo, errorHandlers) if !ok { utils.Error("Failed to send registration success: type assertion failed") return } - tierName := ch.deps.TierManager.GetTierName(info.Tier) - colorCode := ch.deps.TierManager.GetTierANSIColor(info.Tier) + tierName := handler.deps.TierManager.GetTierName(info.Tier) + colorCode := handler.deps.TierManager.GetTierANSIColor(info.Tier) // 사용자 리그 결정 및 이름 가져오기 - userLeague := ch.deps.ScoreCalculator.GetUserLeague(info.Tier) - leagueName := ch.deps.ScoreCalculator.GetLeagueName(userLeague) + userLeague := handler.deps.ScoreCalculator.GetUserLeague(info.Tier) + leagueName := handler.deps.ScoreCalculator.GetLeagueName(userLeague) response := fmt.Sprintf("```ansi\n"+constants.MsgRegisterSuccess+"\n```", - colorCode, name, tierName, ch.deps.TierManager.GetANSIReset(), leagueName) + colorCode, name, tierName, handler.deps.TierManager.GetANSIReset(), leagueName) - if _, err := s.ChannelMessageSend(channelID, response); err != nil { + if _, err := session.ChannelMessageSend(channelID, response); err != nil { utils.Error("DISCORD API ERROR: Failed to send registration response: %v", err) } } -func (ch *CommandHandler) handleScoreboard(s *discordgo.Session, m *discordgo.MessageCreate) { - errorHandlers := utils.NewErrorHandlerFactory(s, m.ChannelID) +func (handler *CommandHandler) handleScoreboard(session *discordgo.Session, message *discordgo.MessageCreate) { + errorHandlers := utils.NewErrorHandlerFactory(session, message.ChannelID) - utils.Info("Scoreboard command received from user: %s (ID: %s)", m.Author.Username, m.Author.ID) - utils.Info("Guild ID: %s, Channel ID: %s", m.GuildID, m.ChannelID) + utils.Info("Scoreboard command received from user: %s (ID: %s)", message.Author.Username, message.Author.ID) + utils.Info("Guild ID: %session, Channel ID: %s", message.GuildID, message.ChannelID) // 관리자 권한 확인 - isAdmin := ch.isAdmin(s, m) - utils.Info("User %s admin status: %t", m.Author.Username, isAdmin) + isAdmin := handler.isAdmin(session, message) + utils.Info("User %s admin status: %t", message.Author.Username, isAdmin) if !isAdmin { - utils.Warn("User %s attempted to use scoreboard without admin permissions", m.Author.Username) + utils.Warn("User %s attempted to use scoreboard without admin permissions", message.Author.Username) errorHandlers.Validation().HandleInsufficientPermissions() return } // 스코어보드 생성 성능 측정 시작 startTime := time.Now() - embed, err := ch.deps.ScoreboardManager.GenerateScoreboard(isAdmin) + embed, err := handler.deps.ScoreboardManager.GenerateScoreboard(isAdmin) duration := time.Since(startTime) // 스코어보드 성능 텔레메트리 전송 - if ch.deps.MetricsClient != nil { - ch.deps.MetricsClient.SendPerformanceMetric("scoreboard_generation", duration, err == nil) + if handler.deps.MetricsClient != nil { + handler.deps.MetricsClient.SendPerformanceMetric("scoreboard_generation", duration, err == nil) } if err != nil { @@ -402,51 +402,51 @@ func (ch *CommandHandler) handleScoreboard(s *discordgo.Session, m *discordgo.Me return } - utils.Info("Scoreboard generated successfully, sending to channel %s", m.ChannelID) + utils.Info("Scoreboard generated successfully, sending to channel %s", message.ChannelID) - if _, err := s.ChannelMessageSendEmbed(m.ChannelID, embed); err != nil { + if _, err := session.ChannelMessageSendEmbed(message.ChannelID, embed); err != nil { utils.Error("DISCORD API ERROR: Failed to send scoreboard embed: %v", err) } else { utils.Info("Scoreboard sent successfully") } } -func (ch *CommandHandler) handleParticipants(s *discordgo.Session, m *discordgo.MessageCreate) { - errorHandlers := utils.NewErrorHandlerFactory(s, m.ChannelID) +func (handler *CommandHandler) handleParticipants(session *discordgo.Session, message *discordgo.MessageCreate) { + errorHandlers := utils.NewErrorHandlerFactory(session, message.ChannelID) // 관리자 권한 확인 - if !ch.isAdmin(s, m) { + if !handler.isAdmin(session, message) { errorHandlers.Validation().HandleInsufficientPermissions() return } - participants := ch.deps.Storage.GetParticipants() + participants := handler.deps.Storage.GetParticipants() if len(participants) == 0 { - errors.SendDiscordInfo(s, m.ChannelID, constants.MsgParticipantsEmpty) + errors.SendDiscordInfo(session, message.ChannelID, constants.MsgParticipantsEmpty) return } - var sb strings.Builder - sb.WriteString("```ansi\n") + var builder strings.Builder + builder.WriteString("```ansi\n") - for i, p := range participants { - tierName := ch.deps.TierManager.GetTierName(p.StartTier) - colorCode := ch.deps.TierManager.GetTierANSIColor(p.StartTier) - sb.WriteString(fmt.Sprintf("%s%d. %s - %s%s\n", - colorCode, i+1, p.BaekjoonID, tierName, ch.deps.TierManager.GetANSIReset())) + for i, participant := range participants { + tierName := handler.deps.TierManager.GetTierName(participant.StartTier) + colorCode := handler.deps.TierManager.GetTierANSIColor(participant.StartTier) + builder.WriteString(fmt.Sprintf("%s%d. %s - %s%s\n", + colorCode, i+1, participant.BaekjoonID, tierName, handler.deps.TierManager.GetANSIReset())) } - sb.WriteString("```") - if _, err := s.ChannelMessageSend(m.ChannelID, sb.String()); err != nil { + builder.WriteString("```") + if _, err := session.ChannelMessageSend(message.ChannelID, builder.String()); err != nil { utils.Error("DISCORD API ERROR: Failed to send participants list: %v", err) } } -func (ch *CommandHandler) handleRemoveParticipant(s *discordgo.Session, m *discordgo.MessageCreate, params []string) { - errorHandlers := utils.NewErrorHandlerFactory(s, m.ChannelID) +func (handler *CommandHandler) handleRemoveParticipant(session *discordgo.Session, message *discordgo.MessageCreate, params []string) { + errorHandlers := utils.NewErrorHandlerFactory(session, message.ChannelID) // 관리자 권한 확인 - if !ch.isAdmin(s, m) { + if !handler.isAdmin(session, message) { errorHandlers.Validation().HandleInsufficientPermissions() return } @@ -470,44 +470,44 @@ func (ch *CommandHandler) handleRemoveParticipant(s *discordgo.Session, m *disco } // 참가자 삭제 - err := ch.deps.Storage.RemoveParticipant(baekjoonID) + err := handler.deps.Storage.RemoveParticipant(baekjoonID) if err != nil { errorHandlers.Data().HandleParticipantNotFound(baekjoonID) return } response := fmt.Sprintf(constants.MsgRemoveSuccess, baekjoonID) - if err := errors.SendDiscordSuccess(s, m.ChannelID, response); err != nil { + if err := errors.SendDiscordSuccess(session, message.ChannelID, response); err != nil { utils.Error("Failed to send participant removal response: %v", err) } } // isAdmin 사용자가 서버 관리자 권한을 가지고 있는지 확인합니다 -func (ch *CommandHandler) isAdmin(s *discordgo.Session, m *discordgo.MessageCreate) bool { +func (handler *CommandHandler) isAdmin(session *discordgo.Session, message *discordgo.MessageCreate) bool { // DM에서는 관리자 권한 없음 - if m.GuildID == "" { + if message.GuildID == "" { utils.Info("User is in DM, no admin permissions") return false } // 길드 정보 가져오기 - guild, err := s.State.Guild(m.GuildID) + guild, err := session.State.Guild(message.GuildID) if err != nil || guild == nil { utils.Warn("Cannot get guild information: %v", err) return false } // 서버 소유자인지 확인 - if m.Author.ID == guild.OwnerID { - utils.Info("User %s is the guild owner - granting admin access", m.Author.Username) + if message.Author.ID == guild.OwnerID { + utils.Info("User %s is the guild owner - granting admin access", message.Author.Username) return true } // 멤버 정보 가져오기 - member, err := s.GuildMember(m.GuildID, m.Author.ID) + member, err := session.GuildMember(message.GuildID, message.Author.ID) if err != nil || member == nil { - utils.Warn("Cannot get member information for %s: %v", m.Author.Username, err) + utils.Warn("Cannot get member information for %s: %v", message.Author.Username, err) return false } @@ -515,7 +515,7 @@ func (ch *CommandHandler) isAdmin(s *discordgo.Session, m *discordgo.MessageCrea // 멤버의 역할들을 확인 for _, roleID := range member.Roles { - role, err := s.State.Role(m.GuildID, roleID) + role, err := session.State.Role(message.GuildID, roleID) if err != nil { utils.Warn("Cannot get role %s: %v", roleID, err) continue @@ -525,29 +525,29 @@ func (ch *CommandHandler) isAdmin(s *discordgo.Session, m *discordgo.MessageCrea // 관리자 권한(ADMINISTRATOR) 확인 if role.Permissions&discordgo.PermissionAdministrator != 0 { - utils.Info("User %s has ADMINISTRATOR permission through role %s - granting admin access", m.Author.Username, role.Name) + utils.Info("User %s has ADMINISTRATOR permission through role %s - granting admin access", message.Author.Username, role.Name) return true } } - utils.Info("User %s has no admin permissions", m.Author.Username) + utils.Info("User %s has no admin permissions", message.Author.Username) return false } // handleCacheStats 캐시 통계를 조회합니다 -func (ch *CommandHandler) handleCacheStats(s *discordgo.Session, m *discordgo.MessageCreate) { - errorHandlers := utils.NewErrorHandlerFactory(s, m.ChannelID) +func (handler *CommandHandler) handleCacheStats(session *discordgo.Session, message *discordgo.MessageCreate) { + errorHandlers := utils.NewErrorHandlerFactory(session, message.ChannelID) // 관리자 권한 확인 - if !ch.isAdmin(s, m) { + if !handler.isAdmin(session, message) { errorHandlers.Validation().HandleInsufficientPermissions() return } - if cachedClient, ok := ch.deps.APIClient.(*api.CachedSolvedACClient); ok { + if cachedClient, ok := handler.deps.APIClient.(*api.CachedSolvedACClient); ok { stats := cachedClient.GetCacheStats() - message := fmt.Sprintf("```\n📊 API Cache Statistics\n\n"+ + statsMessage := fmt.Sprintf("```\n📊 API Cache Statistics\n\n"+ "Total API Calls: %d\n"+ "Cache Hits: %d\n"+ "Cache Misses: %d\n"+ @@ -559,11 +559,11 @@ func (ch *CommandHandler) handleCacheStats(s *discordgo.Session, m *discordgo.Me stats.TotalCalls, stats.CacheHits, stats.CacheMisses, stats.HitRate, stats.UserInfoCached, stats.UserTop100Cached, stats.UserAdditionalCached) - if err := errors.SendDiscordInfo(s, m.ChannelID, message); err != nil { + if err := errors.SendDiscordInfo(session, message.ChannelID, statsMessage); err != nil { utils.Error("Failed to send cache stats response: %v", err) } } else { - if err := errors.SendDiscordWarning(s, m.ChannelID, "캐시가 비활성화되어 있습니다."); err != nil { + if err := errors.SendDiscordWarning(session, message.ChannelID, "캐시가 비활성화되어 있습니다."); err != nil { utils.Error("Failed to send cache disabled warning: %v", err) } } diff --git a/bot/scoreboard.go b/bot/scoreboard.go index 820f315..7b52082 100644 --- a/bot/scoreboard.go +++ b/bot/scoreboard.go @@ -37,12 +37,12 @@ func NewScoreboardManager(storage interfaces.StorageRepository, calculator inter } } -func (sm *ScoreboardManager) GetStorage() interfaces.StorageRepository { - return sm.storage +func (manager *ScoreboardManager) GetStorage() interfaces.StorageRepository { + return manager.storage } -func (sm *ScoreboardManager) GenerateScoreboard(isAdmin bool) (*discordgo.MessageEmbed, error) { - competition := sm.storage.GetCompetition() +func (manager *ScoreboardManager) GenerateScoreboard(isAdmin bool) (*discordgo.MessageEmbed, error) { + competition := manager.storage.GetCompetition() if competition == nil || !competition.IsActive { return nil, fmt.Errorf("활성화된 대회가 없습니다") } @@ -54,67 +54,67 @@ func (sm *ScoreboardManager) GenerateScoreboard(isAdmin bool) (*discordgo.Messag now.Day() == competition.EndDate.Day() // 블랙아웃 체크 (마지막날에는 공개) - if embed := sm.checkBlackoutPeriod(competition, isAdmin || isLastDay); embed != nil { + if embed := manager.checkBlackoutPeriod(competition, isAdmin || isLastDay); embed != nil { return embed, nil } // 참가자 체크 - participants := sm.storage.GetParticipants() - if embed := sm.checkEmptyParticipants(competition, participants); embed != nil { + participants := manager.storage.GetParticipants() + if embed := manager.checkEmptyParticipants(competition, participants); embed != nil { return embed, nil } // 점수 데이터 수집 - scores, err := sm.collectScoreData(participants) + scores, err := manager.collectScoreData(participants) if err != nil { return nil, err } // 포맷팅 - return sm.formatScoreboard(competition, scores, isAdmin), nil + return manager.formatScoreboard(competition, scores, isAdmin), nil } // CollectScoreData 참가자들의 점수 데이터를 수집하여 반환합니다 (외부 접근용) -func (sm *ScoreboardManager) CollectScoreData() ([]models.ScoreData, error) { - competition := sm.storage.GetCompetition() +func (manager *ScoreboardManager) CollectScoreData() ([]models.ScoreData, error) { + competition := manager.storage.GetCompetition() if competition == nil || !competition.IsActive { return nil, fmt.Errorf("활성화된 대회가 없습니다") } - participants := sm.storage.GetParticipants() + participants := manager.storage.GetParticipants() if len(participants) == 0 { return []models.ScoreData{}, nil } - return sm.collectScoreData(participants) + return manager.collectScoreData(participants) } // checkBlackoutPeriod 블랙아웃 기간인지 확인하고 해당 embed 반환 -func (sm *ScoreboardManager) checkBlackoutPeriod(competition *models.Competition, isAdmin bool) *discordgo.MessageEmbed { - if sm.storage.IsBlackoutPeriod() && !isAdmin { +func (manager *ScoreboardManager) checkBlackoutPeriod(competition *models.Competition, isAdmin bool) *discordgo.MessageEmbed { + if manager.storage.IsBlackoutPeriod() && !isAdmin { return &discordgo.MessageEmbed{ Title: constants.MsgScoreboardBlackout, Description: constants.MsgScoreboardBlackoutDesc, - Color: sm.tierManager.GetTierColor(0), // Unranked color + Color: manager.tierManager.GetTierColor(0), // Unranked color } } return nil } // checkEmptyParticipants 참가자가 없는지 확인하고 해당 embed 반환 -func (sm *ScoreboardManager) checkEmptyParticipants(competition *models.Competition, participants []models.Participant) *discordgo.MessageEmbed { +func (manager *ScoreboardManager) checkEmptyParticipants(competition *models.Competition, participants []models.Participant) *discordgo.MessageEmbed { if len(participants) == 0 { return &discordgo.MessageEmbed{ Title: fmt.Sprintf(constants.MsgScoreboardTitle, competition.Name), Description: constants.MsgScoreboardNoParticipants, - Color: sm.tierManager.GetTierColor(0), // Unranked color + Color: manager.tierManager.GetTierColor(0), // Unranked color } } return nil } // collectScoreData 참가자들의 점수 데이터를 병렬로 수집합니다 -func (sm *ScoreboardManager) collectScoreData(participants []models.Participant) ([]models.ScoreData, error) { +func (manager *ScoreboardManager) collectScoreData(participants []models.Participant) ([]models.ScoreData, error) { if len(participants) == 0 { return []models.ScoreData{}, nil } @@ -127,7 +127,7 @@ func (sm *ScoreboardManager) collectScoreData(participants []models.Participant) scoreChan := performance.GetScoreDataChannel(len(participants)) defer performance.PutScoreDataChannel(scoreChan) - semaphore := performance.GetSemaphoreChannel(sm.concurrencyManager.GetCurrentLimit()) + semaphore := performance.GetSemaphoreChannel(manager.concurrencyManager.GetCurrentLimit()) defer performance.PutSemaphoreChannel(semaphore) var wg sync.WaitGroup @@ -142,11 +142,11 @@ func (sm *ScoreboardManager) collectScoreData(participants []models.Participant) defer func() { <-semaphore }() startTime := time.Now() - scoreData, err := sm.calculateParticipantScore(p) + scoreData, err := manager.calculateParticipantScore(p) responseTime := time.Since(startTime) // 응답 시간을 적응형 동시성 관리자에 기록 - sm.concurrencyManager.RecordResponseTime(responseTime) + manager.concurrencyManager.RecordResponseTime(responseTime) if err != nil { utils.Warn("Failed to calculate score for participant %s: %v", p.Name, err) @@ -177,19 +177,19 @@ func (sm *ScoreboardManager) collectScoreData(participants []models.Participant) } // calculateParticipantScore 개별 참가자의 점수를 계산합니다 -func (sm *ScoreboardManager) calculateParticipantScore(participant models.Participant) (models.ScoreData, error) { +func (manager *ScoreboardManager) calculateParticipantScore(participant models.Participant) (models.ScoreData, error) { ctx := context.Background() - userInfo, err := sm.client.GetUserInfo(ctx, participant.BaekjoonID) + userInfo, err := manager.client.GetUserInfo(ctx, participant.BaekjoonID) if err != nil { return models.ScoreData{}, err } - top100, err := sm.client.GetUserTop100(ctx, participant.BaekjoonID) + top100, err := manager.client.GetUserTop100(ctx, participant.BaekjoonID) if err != nil { return models.ScoreData{}, err } - rawScore := sm.calculator.CalculateScoreWithTop100(top100, participant.StartTier, participant.StartProblemIDs) + rawScore := manager.calculator.CalculateScoreWithTop100(top100, participant.StartTier, participant.StartProblemIDs) roundedScore := math.Round(rawScore) newProblemCount := top100.Count - participant.StartProblemCount @@ -203,7 +203,7 @@ func (sm *ScoreboardManager) calculateParticipantScore(participant models.Partic BaekjoonID: participant.BaekjoonID, Score: roundedScore, RawScore: rawScore, - League: sm.calculator.GetUserLeague(participant.StartTier), + League: manager.calculator.GetUserLeague(participant.StartTier), CurrentTier: userInfo.Tier, CurrentRating: userInfo.Rating, ProblemCount: newProblemCount, @@ -211,7 +211,7 @@ func (sm *ScoreboardManager) calculateParticipantScore(participant models.Partic } // groupScoresByLeague 참가자들을 리그별로 분류하고 점수 순으로 정렬합니다 -func (sm *ScoreboardManager) groupScoresByLeague(scores []models.ScoreData) map[int][]models.ScoreData { +func (manager *ScoreboardManager) groupScoresByLeague(scores []models.ScoreData) map[int][]models.ScoreData { leagueScores := make(map[int][]models.ScoreData) for _, score := range scores { @@ -234,7 +234,7 @@ func (sm *ScoreboardManager) groupScoresByLeague(scores []models.ScoreData) map[ } // formatScoreboard 점수 데이터를 포맷팅하여 Discord 임베드 메시지로 반환합니다 -func (sm *ScoreboardManager) formatScoreboard(competition *models.Competition, scores []models.ScoreData, isAdmin bool) *discordgo.MessageEmbed { +func (manager *ScoreboardManager) formatScoreboard(competition *models.Competition, scores []models.ScoreData, isAdmin bool) *discordgo.MessageEmbed { embed := &discordgo.MessageEmbed{ Title: fmt.Sprintf(constants.MsgScoreboardTitle, competition.Name), Description: fmt.Sprintf("%s ~ %s", @@ -248,9 +248,9 @@ func (sm *ScoreboardManager) formatScoreboard(competition *models.Competition, s return embed } - leagueScores := sm.groupScoresByLeague(scores) + leagueScores := manager.groupScoresByLeague(scores) - var sb strings.Builder + var builder strings.Builder leagueOrder := []int{constants.LeagueRookie, constants.LeaguePro, constants.LeagueMaster} @@ -259,14 +259,14 @@ func (sm *ScoreboardManager) formatScoreboard(competition *models.Competition, s continue } - leagueName := sm.calculator.GetLeagueName(league) - sb.WriteString(fmt.Sprintf("\n**🏆 %s 리그**\n", leagueName)) - sb.WriteString("```\n") - sb.WriteString(fmt.Sprintf("%-*s %-*s %*s\n", + leagueName := manager.calculator.GetLeagueName(league) + builder.WriteString(fmt.Sprintf("\n**🏆 %s 리그**\n", leagueName)) + builder.WriteString("```\n") + builder.WriteString(fmt.Sprintf("%-*s %-*s %*s\n", constants.ScoreboardRankWidth, "순위", constants.ScoreboardNameWidth, "아이디", constants.ScoreboardScoreWidth, "점수")) - sb.WriteString(constants.ScoreboardSeparator + "\n") + builder.WriteString(constants.ScoreboardSeparator + "\n") var lastRawScore float64 = -1.0 var rank int @@ -274,16 +274,16 @@ func (sm *ScoreboardManager) formatScoreboard(competition *models.Competition, s if score.RawScore != lastRawScore { rank = i + 1 } - sb.WriteString(fmt.Sprintf("%-*d %-*s %*.0f\n", + builder.WriteString(fmt.Sprintf("%-*d %-*s %*.0f\n", constants.ScoreboardRankWidth, rank, constants.ScoreboardNameWidth, utils.TruncateString(score.BaekjoonID, constants.ScoreboardNameWidth), constants.ScoreboardScoreWidth, score.Score)) lastRawScore = score.RawScore } - sb.WriteString("```\n") + builder.WriteString("```\n") } - embed.Description += sb.String() + embed.Description += builder.String() now := utils.GetCurrentTimeKST() if now.Before(competition.BlackoutStartDate) { @@ -297,8 +297,8 @@ func (sm *ScoreboardManager) formatScoreboard(competition *models.Competition, s } // SendDailyScoreboard 매일 스코어보드를 지정된 채널에 전송합니다 -func (sm *ScoreboardManager) SendDailyScoreboard(session *discordgo.Session, channelID string) error { - embed, err := sm.GenerateScoreboard(false) // 자동 스코어보드는 관리자 권한 없음 +func (manager *ScoreboardManager) SendDailyScoreboard(session *discordgo.Session, channelID string) error { + embed, err := manager.GenerateScoreboard(false) // 자동 스코어보드는 관리자 권한 없음 if err != nil { return err } diff --git a/cache/cache.go b/cache/cache.go index b86ed29..568618e 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -16,8 +16,8 @@ type CacheItem struct { } // IsExpired 캐시 아이템이 만료되었는지 확인합니다 -func (c *CacheItem) IsExpired() bool { - return time.Now().After(c.ExpiresAt) +func (item *CacheItem) IsExpired() bool { + return time.Now().After(item.ExpiresAt) } // CacheStats 캐시 통계 정보를 나타냅니다 @@ -39,32 +39,32 @@ type ExpirationEntry struct { // ExpirationQueue 만료 시간 기반 우선순위 큐 (최소 힙) type ExpirationQueue []*ExpirationEntry -func (pq ExpirationQueue) Len() int { return len(pq) } +func (priorityQueue ExpirationQueue) Len() int { return len(priorityQueue) } -func (pq ExpirationQueue) Less(i, j int) bool { - return pq[i].ExpiresAt.Before(pq[j].ExpiresAt) +func (priorityQueue ExpirationQueue) Less(i, j int) bool { + return priorityQueue[i].ExpiresAt.Before(priorityQueue[j].ExpiresAt) } -func (pq ExpirationQueue) Swap(i, j int) { - pq[i], pq[j] = pq[j], pq[i] - pq[i].Index = i - pq[j].Index = j +func (priorityQueue ExpirationQueue) Swap(i, j int) { + priorityQueue[i], priorityQueue[j] = priorityQueue[j], priorityQueue[i] + priorityQueue[i].Index = i + priorityQueue[j].Index = j } -func (pq *ExpirationQueue) Push(x interface{}) { - n := len(*pq) +func (priorityQueue *ExpirationQueue) Push(x interface{}) { + n := len(*priorityQueue) entry := x.(*ExpirationEntry) entry.Index = n - *pq = append(*pq, entry) + *priorityQueue = append(*priorityQueue, entry) } -func (pq *ExpirationQueue) Pop() interface{} { - old := *pq +func (priorityQueue *ExpirationQueue) Pop() interface{} { + old := *priorityQueue n := len(old) entry := old[n-1] old[n-1] = nil entry.Index = -1 - *pq = old[0 : n-1] + *priorityQueue = old[0 : n-1] return entry } @@ -95,8 +95,8 @@ type EfficientAPICache struct { // NewEfficientAPICache 새로운 EfficientAPICache 인스턴스를 생성합니다 func NewEfficientAPICache() *EfficientAPICache { - pq := &ExpirationQueue{} - heap.Init(pq) + priorityQueue := &ExpirationQueue{} + heap.Init(priorityQueue) return &EfficientAPICache{ userInfoCache: make(map[string]*CacheItem), @@ -104,7 +104,7 @@ func NewEfficientAPICache() *EfficientAPICache { userAdditionalCache: make(map[string]*CacheItem), userOrganizationsCache: make(map[string]*CacheItem), - expirationQueue: pq, + expirationQueue: priorityQueue, keyToEntry: make(map[string]*ExpirationEntry), // 캐시 TTL 설정 @@ -121,9 +121,9 @@ func NewEfficientAPICache() *EfficientAPICache { } // setWithExpiration 공통 저장 로직 (우선순위 큐에도 추가) -func (c *EfficientAPICache) setWithExpiration(cacheType, key string, data interface{}, ttl time.Duration) { - c.mu.Lock() - defer c.mu.Unlock() +func (cache *EfficientAPICache) setWithExpiration(cacheType, key string, data interface{}, ttl time.Duration) { + cache.mu.Lock() + defer cache.mu.Unlock() expiresAt := time.Now().Add(ttl) item := &CacheItem{ @@ -132,7 +132,7 @@ func (c *EfficientAPICache) setWithExpiration(cacheType, key string, data interf } // 기존 항목이 있다면 우선순위 큐에서 제거 - if existingEntry, exists := c.keyToEntry[key]; exists { + if existingEntry, exists := cache.keyToEntry[key]; exists { // 힙에서 제거하지 않고 무효화 처리 (성능상 이유) existingEntry.ExpiresAt = time.Time{} // 무효화 마크 } @@ -140,13 +140,13 @@ func (c *EfficientAPICache) setWithExpiration(cacheType, key string, data interf // 캐시 맵에 저장 switch cacheType { case "userInfo": - c.userInfoCache[key] = item + cache.userInfoCache[key] = item case "userTop100": - c.userTop100Cache[key] = item + cache.userTop100Cache[key] = item case "userAdditional": - c.userAdditionalCache[key] = item + cache.userAdditionalCache[key] = item case "userOrganizations": - c.userOrganizationsCache[key] = item + cache.userOrganizationsCache[key] = item } // 우선순위 큐에 추가 @@ -155,16 +155,16 @@ func (c *EfficientAPICache) setWithExpiration(cacheType, key string, data interf CacheType: cacheType, ExpiresAt: expiresAt, } - heap.Push(c.expirationQueue, entry) - c.keyToEntry[key] = entry + heap.Push(cache.expirationQueue, entry) + cache.keyToEntry[key] = entry } // GetUserInfo 캐시에서 사용자 정보를 조회합니다 -func (c *EfficientAPICache) GetUserInfo(handle string) (interface{}, bool) { - c.mu.RLock() - defer c.mu.RUnlock() +func (cache *EfficientAPICache) GetUserInfo(handle string) (interface{}, bool) { + cache.mu.RLock() + defer cache.mu.RUnlock() - item, exists := c.userInfoCache[handle] + item, exists := cache.userInfoCache[handle] if !exists || item.IsExpired() { return nil, false } @@ -173,16 +173,16 @@ func (c *EfficientAPICache) GetUserInfo(handle string) (interface{}, bool) { } // SetUserInfo 사용자 정보를 캐시에 저장합니다 -func (c *EfficientAPICache) SetUserInfo(handle string, userInfo interface{}) { - c.setWithExpiration("userInfo", handle, userInfo, c.userInfoTTL) +func (cache *EfficientAPICache) SetUserInfo(handle string, userInfo interface{}) { + cache.setWithExpiration("userInfo", handle, userInfo, cache.userInfoTTL) } // GetUserTop100 캐시에서 사용자 TOP 100을 조회합니다 -func (c *EfficientAPICache) GetUserTop100(handle string) (interface{}, bool) { - c.mu.RLock() - defer c.mu.RUnlock() +func (cache *EfficientAPICache) GetUserTop100(handle string) (interface{}, bool) { + cache.mu.RLock() + defer cache.mu.RUnlock() - item, exists := c.userTop100Cache[handle] + item, exists := cache.userTop100Cache[handle] if !exists || item.IsExpired() { return nil, false } @@ -191,16 +191,16 @@ func (c *EfficientAPICache) GetUserTop100(handle string) (interface{}, bool) { } // SetUserTop100 사용자 TOP 100을 캐시에 저장합니다 -func (c *EfficientAPICache) SetUserTop100(handle string, top100 interface{}) { - c.setWithExpiration("userTop100", handle, top100, c.userTop100TTL) +func (cache *EfficientAPICache) SetUserTop100(handle string, top100 interface{}) { + cache.setWithExpiration("userTop100", handle, top100, cache.userTop100TTL) } // GetUserAdditionalInfo 캐시에서 사용자 추가 정보를 조회합니다 -func (c *EfficientAPICache) GetUserAdditionalInfo(handle string) (interface{}, bool) { - c.mu.RLock() - defer c.mu.RUnlock() +func (cache *EfficientAPICache) GetUserAdditionalInfo(handle string) (interface{}, bool) { + cache.mu.RLock() + defer cache.mu.RUnlock() - item, exists := c.userAdditionalCache[handle] + item, exists := cache.userAdditionalCache[handle] if !exists || item.IsExpired() { return nil, false } @@ -209,16 +209,16 @@ func (c *EfficientAPICache) GetUserAdditionalInfo(handle string) (interface{}, b } // SetUserAdditionalInfo 사용자 추가 정보를 캐시에 저장합니다 -func (c *EfficientAPICache) SetUserAdditionalInfo(handle string, additionalInfo interface{}) { - c.setWithExpiration("userAdditional", handle, additionalInfo, c.userAdditionalTTL) +func (cache *EfficientAPICache) SetUserAdditionalInfo(handle string, additionalInfo interface{}) { + cache.setWithExpiration("userAdditional", handle, additionalInfo, cache.userAdditionalTTL) } // GetUserOrganizations 캐시에서 사용자 조직 정보를 조회합니다 -func (c *EfficientAPICache) GetUserOrganizations(handle string) (interface{}, bool) { - c.mu.RLock() - defer c.mu.RUnlock() +func (cache *EfficientAPICache) GetUserOrganizations(handle string) (interface{}, bool) { + cache.mu.RLock() + defer cache.mu.RUnlock() - item, exists := c.userOrganizationsCache[handle] + item, exists := cache.userOrganizationsCache[handle] if !exists || item.IsExpired() { return nil, false } @@ -227,34 +227,34 @@ func (c *EfficientAPICache) GetUserOrganizations(handle string) (interface{}, bo } // SetUserOrganizations 사용자 조직 정보를 캐시에 저장합니다 -func (c *EfficientAPICache) SetUserOrganizations(handle string, organizations interface{}) { - c.setWithExpiration("userOrganizations", handle, organizations, c.userOrganizationsTTL) +func (cache *EfficientAPICache) SetUserOrganizations(handle string, organizations interface{}) { + cache.setWithExpiration("userOrganizations", handle, organizations, cache.userOrganizationsTTL) } // ClearExpiredEfficient 우선순위 큐를 사용하여 효율적으로 만료된 항목을 정리합니다 -func (c *EfficientAPICache) ClearExpiredEfficient() int { - c.mu.Lock() - defer c.mu.Unlock() +func (cache *EfficientAPICache) ClearExpiredEfficient() int { + cache.mu.Lock() + defer cache.mu.Unlock() now := time.Now() startTime := time.Now() cleaned := 0 // 시간 제한과 배치 크기 제한으로 정리 - for cleaned < c.cleanupBatchSize && time.Since(startTime) < c.maxCleanupDuration { - if c.expirationQueue.Len() == 0 { + for cleaned < cache.cleanupBatchSize && time.Since(startTime) < cache.maxCleanupDuration { + if cache.expirationQueue.Len() == 0 { break } // 가장 빨리 만료되는 항목 확인 - entry := (*c.expirationQueue)[0] + entry := (*cache.expirationQueue)[0] // 무효화된 항목이거나 아직 만료되지 않은 경우 if entry.ExpiresAt.IsZero() || now.Before(entry.ExpiresAt) { if entry.ExpiresAt.IsZero() { // 무효화된 항목은 제거 - heap.Pop(c.expirationQueue) - delete(c.keyToEntry, entry.Key) + heap.Pop(cache.expirationQueue) + delete(cache.keyToEntry, entry.Key) cleaned++ } else { // 아직 만료되지 않았으므로 정리 중단 @@ -264,59 +264,59 @@ func (c *EfficientAPICache) ClearExpiredEfficient() int { } // 만료된 항목 제거 - heap.Pop(c.expirationQueue) - delete(c.keyToEntry, entry.Key) + heap.Pop(cache.expirationQueue) + delete(cache.keyToEntry, entry.Key) // 해당 캐시 맵에서도 제거 switch entry.CacheType { case "userInfo": - delete(c.userInfoCache, entry.Key) + delete(cache.userInfoCache, entry.Key) case "userTop100": - delete(c.userTop100Cache, entry.Key) + delete(cache.userTop100Cache, entry.Key) case "userAdditional": - delete(c.userAdditionalCache, entry.Key) + delete(cache.userAdditionalCache, entry.Key) case "userOrganizations": - delete(c.userOrganizationsCache, entry.Key) + delete(cache.userOrganizationsCache, entry.Key) } cleaned++ } - c.lastCleanup = now + cache.lastCleanup = now return cleaned } // GetStats 캐시 통계를 반환합니다 -func (c *EfficientAPICache) GetStats() CacheStats { - c.mu.RLock() - defer c.mu.RUnlock() +func (cache *EfficientAPICache) GetStats() CacheStats { + cache.mu.RLock() + defer cache.mu.RUnlock() return CacheStats{ - UserInfoCount: len(c.userInfoCache), - UserTop100Count: len(c.userTop100Cache), - UserAdditionalCount: len(c.userAdditionalCache), - UserOrganizationsCount: len(c.userOrganizationsCache), + UserInfoCount: len(cache.userInfoCache), + UserTop100Count: len(cache.userTop100Cache), + UserAdditionalCount: len(cache.userAdditionalCache), + UserOrganizationsCount: len(cache.userOrganizationsCache), } } // Clear 모든 캐시를 삭제합니다 -func (c *EfficientAPICache) Clear() { - c.mu.Lock() - defer c.mu.Unlock() +func (cache *EfficientAPICache) Clear() { + cache.mu.Lock() + defer cache.mu.Unlock() - c.userInfoCache = make(map[string]*CacheItem) - c.userTop100Cache = make(map[string]*CacheItem) - c.userAdditionalCache = make(map[string]*CacheItem) - c.userOrganizationsCache = make(map[string]*CacheItem) + cache.userInfoCache = make(map[string]*CacheItem) + cache.userTop100Cache = make(map[string]*CacheItem) + cache.userAdditionalCache = make(map[string]*CacheItem) + cache.userOrganizationsCache = make(map[string]*CacheItem) // 우선순위 큐와 인덱스도 초기화 - c.expirationQueue = &ExpirationQueue{} - heap.Init(c.expirationQueue) - c.keyToEntry = make(map[string]*ExpirationEntry) + cache.expirationQueue = &ExpirationQueue{} + heap.Init(cache.expirationQueue) + cache.keyToEntry = make(map[string]*ExpirationEntry) } // StartEfficientCleanupWorker 효율적인 캐시 정리 워커를 시작합니다 -func (c *EfficientAPICache) StartEfficientCleanupWorker(interval time.Duration) context.CancelFunc { +func (cache *EfficientAPICache) StartEfficientCleanupWorker(interval time.Duration) context.CancelFunc { ctx, cancel := context.WithCancel(context.Background()) ticker := time.NewTicker(interval) @@ -325,7 +325,7 @@ func (c *EfficientAPICache) StartEfficientCleanupWorker(interval time.Duration) for { select { case <-ticker.C: - cleaned := c.ClearExpiredEfficient() + cleaned := cache.ClearExpiredEfficient() if cleaned > 0 { // 로깅은 순환 참조 방지를 위해 제거 // utils.Debug("Cleaned %d expired cache entries", cleaned) diff --git a/performance/adaptive_concurrency.go b/performance/adaptive_concurrency.go index 490a552..2c66321 100644 --- a/performance/adaptive_concurrency.go +++ b/performance/adaptive_concurrency.go @@ -39,85 +39,85 @@ func NewAdaptiveConcurrencyManager() *AdaptiveConcurrencyManager { } // GetCurrentLimit 현재 동시성 제한을 반환합니다 -func (acm *AdaptiveConcurrencyManager) GetCurrentLimit() int { - acm.mutex.RLock() - defer acm.mutex.RUnlock() - return acm.currentLimit +func (manager *AdaptiveConcurrencyManager) GetCurrentLimit() int { + manager.mutex.RLock() + defer manager.mutex.RUnlock() + return manager.currentLimit } // RecordResponseTime API 응답 시간을 기록하고 필요시 동시성을 조정합니다 -func (acm *AdaptiveConcurrencyManager) RecordResponseTime(responseTime time.Duration) { - acm.mutex.Lock() - defer acm.mutex.Unlock() +func (manager *AdaptiveConcurrencyManager) RecordResponseTime(responseTime time.Duration) { + manager.mutex.Lock() + defer manager.mutex.Unlock() // 응답 시간 윈도우에 추가 - acm.responseTimeWindow = append(acm.responseTimeWindow, responseTime) - if len(acm.responseTimeWindow) > acm.windowSize { - acm.responseTimeWindow = acm.responseTimeWindow[1:] + manager.responseTimeWindow = append(manager.responseTimeWindow, responseTime) + if len(manager.responseTimeWindow) > manager.windowSize { + manager.responseTimeWindow = manager.responseTimeWindow[1:] } // 충분한 데이터가 있고 쿨다운이 지났으면 조정 시도 - if len(acm.responseTimeWindow) >= constants.MinResponseTimeWindowSize && time.Since(acm.lastAdjustment) > acm.adjustmentCooldown { - acm.adjustConcurrency() + if len(manager.responseTimeWindow) >= constants.MinResponseTimeWindowSize && time.Since(manager.lastAdjustment) > manager.adjustmentCooldown { + manager.adjustConcurrency() } } // adjustConcurrency 응답 시간 통계를 기반으로 동시성을 조정합니다 -func (acm *AdaptiveConcurrencyManager) adjustConcurrency() { - avgResponseTime := acm.calculateAverageResponseTime() - p95ResponseTime := acm.calculateP95ResponseTime() +func (manager *AdaptiveConcurrencyManager) adjustConcurrency() { + avgResponseTime := manager.calculateAverageResponseTime() + p95ResponseTime := manager.calculateP95ResponseTime() - oldLimit := acm.currentLimit + oldLimit := manager.currentLimit // 응답 시간이 너무 느리면 동시성 감소 - if p95ResponseTime > acm.decreaseThreshold || avgResponseTime > acm.adjustmentThreshold { - if acm.currentLimit > acm.minLimit { - acm.currentLimit = max(acm.minLimit, acm.currentLimit-1) - acm.successiveDecreases++ - acm.successiveIncreases = 0 + if p95ResponseTime > manager.decreaseThreshold || avgResponseTime > manager.adjustmentThreshold { + if manager.currentLimit > manager.minLimit { + manager.currentLimit = max(manager.minLimit, manager.currentLimit-1) + manager.successiveDecreases++ + manager.successiveIncreases = 0 } - } else if avgResponseTime < acm.adjustmentThreshold/2 { + } else if avgResponseTime < manager.adjustmentThreshold/2 { // 응답 시간이 충분히 빠르고 연속으로 성능이 좋으면 동시성 증가 - if acm.currentLimit < acm.maxLimit && acm.successiveDecreases == 0 { + if manager.currentLimit < manager.maxLimit && manager.successiveDecreases == 0 { // 보수적으로 증가 (연속 증가 횟수에 따라 제한) - if acm.successiveIncreases < constants.MaxSuccessiveIncreases { - acm.currentLimit = min(acm.maxLimit, acm.currentLimit+1) - acm.successiveIncreases++ + if manager.successiveIncreases < constants.MaxSuccessiveIncreases { + manager.currentLimit = min(manager.maxLimit, manager.currentLimit+1) + manager.successiveIncreases++ } } - acm.successiveDecreases = 0 + manager.successiveDecreases = 0 } - if oldLimit != acm.currentLimit { - acm.lastAdjustment = time.Now() + if oldLimit != manager.currentLimit { + manager.lastAdjustment = time.Now() // 로깅은 utils 패키지 순환 참조 방지를 위해 제거 } } // calculateAverageResponseTime 평균 응답 시간을 계산합니다 -func (acm *AdaptiveConcurrencyManager) calculateAverageResponseTime() time.Duration { - if len(acm.responseTimeWindow) == 0 { +func (manager *AdaptiveConcurrencyManager) calculateAverageResponseTime() time.Duration { + if len(manager.responseTimeWindow) == 0 { return 0 } var total time.Duration - for _, rt := range acm.responseTimeWindow { - total += rt + for _, responseTime := range manager.responseTimeWindow { + total += responseTime } - return total / time.Duration(len(acm.responseTimeWindow)) + return total / time.Duration(len(manager.responseTimeWindow)) } // calculateP95ResponseTime 95 퍼센타일 응답 시간을 계산합니다 -func (acm *AdaptiveConcurrencyManager) calculateP95ResponseTime() time.Duration { - if len(acm.responseTimeWindow) == 0 { +func (manager *AdaptiveConcurrencyManager) calculateP95ResponseTime() time.Duration { + if len(manager.responseTimeWindow) == 0 { return 0 } // 간단한 95 퍼센타일 계산 (정렬 없이) var maxTime time.Duration - for _, rt := range acm.responseTimeWindow { - if rt > maxTime { - maxTime = rt + for _, responseTime := range manager.responseTimeWindow { + if responseTime > maxTime { + maxTime = responseTime } } @@ -128,20 +128,20 @@ func (acm *AdaptiveConcurrencyManager) calculateP95ResponseTime() time.Duration } // GetStats 현재 통계를 반환합니다 -func (acm *AdaptiveConcurrencyManager) GetStats() ConcurrencyStats { - acm.mutex.RLock() - defer acm.mutex.RUnlock() +func (manager *AdaptiveConcurrencyManager) GetStats() ConcurrencyStats { + manager.mutex.RLock() + defer manager.mutex.RUnlock() return ConcurrencyStats{ - CurrentLimit: acm.currentLimit, - MinLimit: acm.minLimit, - MaxLimit: acm.maxLimit, - AverageResponse: acm.calculateAverageResponseTime(), - P95Response: acm.calculateP95ResponseTime(), - WindowSize: len(acm.responseTimeWindow), - LastAdjustment: acm.lastAdjustment, - SuccessiveInc: acm.successiveIncreases, - SuccessiveDec: acm.successiveDecreases, + CurrentLimit: manager.currentLimit, + MinLimit: manager.minLimit, + MaxLimit: manager.maxLimit, + AverageResponse: manager.calculateAverageResponseTime(), + P95Response: manager.calculateP95ResponseTime(), + WindowSize: len(manager.responseTimeWindow), + LastAdjustment: manager.lastAdjustment, + SuccessiveInc: manager.successiveIncreases, + SuccessiveDec: manager.successiveDecreases, } } diff --git a/scoring/calculator.go b/scoring/calculator.go index 4f09976..53a8e64 100644 --- a/scoring/calculator.go +++ b/scoring/calculator.go @@ -22,16 +22,16 @@ func NewScoreCalculator(apiClient interfaces.APIClient, tierManager *models.Tier } } -func (sc *ScoreCalculator) CalculateScore(ctx context.Context, handle string, startTier int, startProblemIDs []int) (float64, error) { - top100, err := sc.client.GetUserTop100(ctx, handle) +func (calculator *ScoreCalculator) CalculateScore(ctx context.Context, handle string, startTier int, startProblemIDs []int) (float64, error) { + top100, err := calculator.client.GetUserTop100(ctx, handle) if err != nil { return 0, err } - return sc.CalculateScoreWithTop100(top100, startTier, startProblemIDs), nil + return calculator.CalculateScoreWithTop100(top100, startTier, startProblemIDs), nil } -func (sc *ScoreCalculator) CalculateScoreWithTop100(top100 *api.Top100Response, startTier int, startProblemIDs []int) float64 { +func (calculator *ScoreCalculator) CalculateScoreWithTop100(top100 *api.Top100Response, startTier int, startProblemIDs []int) float64 { // 시작 시점 문제 ID들을 맵으로 변환 startProblemsMap := make(map[int]bool) for _, id := range startProblemIDs { @@ -39,7 +39,7 @@ func (sc *ScoreCalculator) CalculateScoreWithTop100(top100 *api.Top100Response, } // 참가자의 리그 결정 (등록 시점 티어 기준) - userLeague := sc.getUserLeague(startTier) + userLeague := calculator.getUserLeague(startTier) totalScore := 0.0 @@ -57,7 +57,7 @@ func (sc *ScoreCalculator) CalculateScoreWithTop100(top100 *api.Top100Response, } // 새로운 가중치 계산 (리그별 + 문제 난이도 vs 시작 티어) - weight := sc.getWeightByLeague(problemLevel, startTier, userLeague) + weight := calculator.getWeightByLeague(problemLevel, startTier, userLeague) score := difficultyValue * weight totalScore += score } @@ -67,7 +67,7 @@ func (sc *ScoreCalculator) CalculateScoreWithTop100(top100 *api.Top100Response, } // getUserLeague 사용자의 등록 시점 티어를 기준으로 리그를 결정합니다 -func (sc *ScoreCalculator) getUserLeague(startTier int) int { +func (calculator *ScoreCalculator) getUserLeague(startTier int) int { // 루키: Unrated ~ Silver V (티어 0-6) if startTier <= 6 { return constants.LeagueRookie @@ -81,21 +81,21 @@ func (sc *ScoreCalculator) getUserLeague(startTier int) int { } // getWeightByLeague 리그별 가중치를 계산합니다 -func (sc *ScoreCalculator) getWeightByLeague(problemLevel, startTier, userLeague int) float64 { +func (calculator *ScoreCalculator) getWeightByLeague(problemLevel, startTier, userLeague int) float64 { switch userLeague { case constants.LeagueRookie: - return sc.getRookieWeight(problemLevel, startTier) + return calculator.getRookieWeight(problemLevel, startTier) case constants.LeaguePro: - return sc.getProWeight(problemLevel, startTier) + return calculator.getProWeight(problemLevel, startTier) case constants.LeagueMaster: - return sc.getMasterWeight(problemLevel, startTier) + return calculator.getMasterWeight(problemLevel, startTier) default: return 1.0 } } // getRookieWeight 루키 리그 가중치를 계산합니다 -func (sc *ScoreCalculator) getRookieWeight(problemLevel, startTier int) float64 { +func (calculator *ScoreCalculator) getRookieWeight(problemLevel, startTier int) float64 { if problemLevel > startTier { return constants.RookieUpperMultiplier // × 1.4 } else if problemLevel == startTier { @@ -106,7 +106,7 @@ func (sc *ScoreCalculator) getRookieWeight(problemLevel, startTier int) float64 } // getProWeight 프로 리그 가중치를 계산합니다 -func (sc *ScoreCalculator) getProWeight(problemLevel, startTier int) float64 { +func (calculator *ScoreCalculator) getProWeight(problemLevel, startTier int) float64 { if problemLevel > startTier { return constants.ProUpperMultiplier // × 1.2 } else if problemLevel == startTier { @@ -117,7 +117,7 @@ func (sc *ScoreCalculator) getProWeight(problemLevel, startTier int) float64 { } // getMasterWeight 마스터 리그 가중치를 계산합니다 -func (sc *ScoreCalculator) getMasterWeight(problemLevel, startTier int) float64 { +func (calculator *ScoreCalculator) getMasterWeight(problemLevel, startTier int) float64 { if problemLevel > startTier { return constants.MasterUpperMultiplier // × 1.0 } else if problemLevel == startTier { @@ -128,7 +128,7 @@ func (sc *ScoreCalculator) getMasterWeight(problemLevel, startTier int) float64 } // GetLeagueName 리그 번호를 리그 이름으로 변환합니다 -func (sc *ScoreCalculator) GetLeagueName(league int) string { +func (calculator *ScoreCalculator) GetLeagueName(league int) string { switch league { case constants.LeagueRookie: return "루키" @@ -142,6 +142,6 @@ func (sc *ScoreCalculator) GetLeagueName(league int) string { } // GetUserLeague 외부에서 사용할 수 있도록 노출합니다 -func (sc *ScoreCalculator) GetUserLeague(startTier int) int { - return sc.getUserLeague(startTier) +func (calculator *ScoreCalculator) GetUserLeague(startTier int) int { + return calculator.getUserLeague(startTier) }