Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
485ed3d
Feat: Implement Tournament Match Detection & Auto Result Recording (#43)
Sunja-An Jan 30, 2026
0b98b46
Feat: Add Load Testing Infrastructure with k6 and Grafana (#44)
Sunja-An Jan 30, 2026
3e84195
Feat: Add Redis Testcontainer Support for Integration Tests (#45)
Sunja-An Jan 30, 2026
51fd502
Chore: Remove mock-mysql binary and add to .gitignore
Sunja-An Jan 30, 2026
4e83d91
Chore: Remove TOURNAMENT_MATCH_DETECTION_SPEC.md from tracking
Sunja-An Jan 30, 2026
c272bbf
Refactor: Update Go module path to github.com/FOR-GAMERS/GAMERS-BE
Sunja-An Jan 30, 2026
f6ac1fc
Fix: Replace log.Fatal with log.Printf in StartContest (#43)
Sunja-An Jan 30, 2026
3ef51d2
Fix: Address medium priority review comments (#43)
Sunja-An Jan 30, 2026
6ab289c
Merge pull request #46 from FOR-GAMERS/feat/#43
Sunja-An Jan 30, 2026
53dc401
Chore: Production Docker & env improvements (#43)
Sunja-An Feb 2, 2026
626a0de
Merge pull request #47 from FOR-GAMERS/feat/#43
Sunja-An Feb 2, 2026
307ac96
Fix: fixing 24 Migration failed
Sunja-An Feb 3, 2026
a4e8772
Merge pull request #48 from FOR-GAMERS/feat/#43
Sunja-An Feb 3, 2026
18d2997
Fix: fixing 24 Migration failed and Cloudflare r2 env added
Sunja-An Feb 3, 2026
9e93711
Merge pull request #49 from FOR-GAMERS/feat/#cloudflare-r3
Sunja-An Feb 3, 2026
36c5a10
Fix: Cross-subdomain cookie sharing and CORS for production domain
Sunja-An Feb 4, 2026
ce78666
Fix: PR Comments fixed
Sunja-An Feb 4, 2026
06f696c
Merge pull request #50 from FOR-GAMERS/feat/#cloudflare-r3
Sunja-An Feb 4, 2026
ce00ed0
Feat: Add Redis caching and parallel execution for Discord API (#51)
Sunja-An Feb 4, 2026
f2c311e
Fix: Add retry, logging, and error differentiation to Valorant API cl…
Sunja-An Feb 4, 2026
90fc8ba
Merge pull request #52 from FOR-GAMERS/feat/#51
Sunja-An Feb 4, 2026
2381771
Feat: Add user role to JWT claims for DB-free admin authorization (#54)
Sunja-An Feb 5, 2026
080a8ef
Merge pull request #55 from FOR-GAMERS/feat/#51
Sunja-An Feb 5, 2026
c2b40fe
Merge origin/main into develop to sync PR #57 changes
Sunja-An Feb 8, 2026
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ env/*.env

# Build outputs
bin/
load-test/mock-mysql/mock-mysql
docs/TOURNAMENT_MATCH_DETECTION_SPEC.md

# Claude Code
CLAUDE.md
Expand Down
52 changes: 29 additions & 23 deletions cmd/server.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
package main

import (
"GAMERS-BE/internal/auth"
authMiddleware "GAMERS-BE/internal/auth/middleware"
"GAMERS-BE/internal/banner"
"GAMERS-BE/internal/comment"
"GAMERS-BE/internal/contest"
"GAMERS-BE/internal/discord"
"GAMERS-BE/internal/game"
"GAMERS-BE/internal/global/common/router"
"GAMERS-BE/internal/global/config"
"GAMERS-BE/internal/global/middleware"
authProvider "GAMERS-BE/internal/global/security/jwt"
"GAMERS-BE/internal/notification"
"GAMERS-BE/internal/oauth2"
"GAMERS-BE/internal/point"
"GAMERS-BE/internal/storage"
"GAMERS-BE/internal/user"
"GAMERS-BE/internal/valorant"
"context"
"log"
"os"

_ "GAMERS-BE/docs"
"github.com/FOR-GAMERS/GAMERS-BE/internal/auth"
authMiddleware "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/middleware"
"github.com/FOR-GAMERS/GAMERS-BE/internal/banner"
"github.com/FOR-GAMERS/GAMERS-BE/internal/comment"
"github.com/FOR-GAMERS/GAMERS-BE/internal/contest"
"github.com/FOR-GAMERS/GAMERS-BE/internal/discord"
"github.com/FOR-GAMERS/GAMERS-BE/internal/game"
"github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/router"
"github.com/FOR-GAMERS/GAMERS-BE/internal/global/config"
"github.com/FOR-GAMERS/GAMERS-BE/internal/global/middleware"
authProvider "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt"
"github.com/FOR-GAMERS/GAMERS-BE/internal/notification"
"github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2"
"github.com/FOR-GAMERS/GAMERS-BE/internal/point"
"github.com/FOR-GAMERS/GAMERS-BE/internal/storage"
"github.com/FOR-GAMERS/GAMERS-BE/internal/user"
"github.com/FOR-GAMERS/GAMERS-BE/internal/valorant"

_ "github.com/FOR-GAMERS/GAMERS-BE/docs"

"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
Expand All @@ -33,6 +34,11 @@ import (
)

func init() {
if os.Getenv("GIN_MODE") == "release" {
log.Println("Production mode detected. Using system environment variables.")
return
}

// Load .env file from env directory
if err := godotenv.Load("env/.env"); err != nil {
log.Println("No env/.env file found, using system environment variables")
Expand Down Expand Up @@ -87,7 +93,8 @@ func main() {
tokenService := authProvider.ProvideJwtService()
authInterceptor := authMiddleware.NewAuthMiddleware(tokenService)

appRouter := router.NewRouter(authInterceptor)
webURL := os.Getenv("WEB_URL")
appRouter := router.NewRouter(authInterceptor, webURL)

authDeps := auth.ProvideAuthDependencies(db, redisClient, &ctx, appRouter)

Expand All @@ -100,9 +107,6 @@ func main() {
// User module - uses OAuth2 repository for Discord avatar URL generation
userDeps := user.ProvideUserDependencies(db, appRouter, oauth2Deps.OAuth2Repository)

// Set user query port for admin middleware after user dependencies are initialized
authInterceptor.SetUserQueryPort(userDeps.UserQueryRepo)

// Game module - provides Game, Team, and GameTeam management
gameDeps := game.ProvideGameDependencies(
db,
Expand All @@ -125,10 +129,12 @@ func main() {
discordDeps.ValidationService,
gameDeps.GameRepository,
gameDeps.TeamRepository,
gameDeps.GameTeamRepository,
)

// Set contest repository for team service (to resolve circular dependency)
// Set contest repository for team service and tournament result service (to resolve circular dependency)
gameDeps.TeamService.SetContestRepository(contestDeps.ContestRepository)
gameDeps.TournamentResultService.SetContestDBPort(contestDeps.ContestRepository)

commentDeps := comment.ProvideCommentDependencies(db, appRouter, contestDeps.ContestRepository)

Expand Down
41 changes: 41 additions & 0 deletions db/migrations/000024_add_match_detection_fields.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Drop tables first (in reverse dependency order)
DROP TABLE IF EXISTS match_player_stats;
DROP TABLE IF EXISTS match_results;

-- Drop indexes conditionally
SET @idx_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND INDEX_NAME = 'idx_games_detection');
SET @sql = IF(@idx_exists > 0, 'DROP INDEX idx_games_detection ON games', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @idx_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND INDEX_NAME = 'idx_games_scheduled');
SET @sql = IF(@idx_exists > 0, 'DROP INDEX idx_games_scheduled ON games', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Drop columns conditionally
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'scheduled_start_time');
SET @sql = IF(@col_exists > 0, 'ALTER TABLE games DROP COLUMN scheduled_start_time', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'detection_window_minutes');
SET @sql = IF(@col_exists > 0, 'ALTER TABLE games DROP COLUMN detection_window_minutes', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'detected_match_id');
SET @sql = IF(@col_exists > 0, 'ALTER TABLE games DROP COLUMN detected_match_id', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'detection_status');
SET @sql = IF(@col_exists > 0, 'ALTER TABLE games DROP COLUMN detection_status', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
88 changes: 88 additions & 0 deletions db/migrations/000024_add_match_detection_fields.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
-- Add match detection fields to games table
-- Note: Using conditional approach to handle partial migrations

-- Add scheduled_start_time if not exists
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'scheduled_start_time');
SET @sql = IF(@col_exists = 0, 'ALTER TABLE games ADD COLUMN scheduled_start_time DATETIME NULL', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Add detection_window_minutes if not exists
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'detection_window_minutes');
SET @sql = IF(@col_exists = 0, 'ALTER TABLE games ADD COLUMN detection_window_minutes INT NOT NULL DEFAULT 120', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Add detected_match_id if not exists
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'detected_match_id');
SET @sql = IF(@col_exists = 0, 'ALTER TABLE games ADD COLUMN detected_match_id VARCHAR(255) NULL', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Add detection_status if not exists
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND COLUMN_NAME = 'detection_status');
SET @sql = IF(@col_exists = 0, "ALTER TABLE games ADD COLUMN detection_status VARCHAR(20) NOT NULL DEFAULT 'NONE'", 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Add idx_games_detection if not exists
SET @idx_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND INDEX_NAME = 'idx_games_detection');
SET @sql = IF(@idx_exists = 0, 'CREATE INDEX idx_games_detection ON games(game_status, detection_status)', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Add idx_games_scheduled if not exists
SET @idx_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'games' AND INDEX_NAME = 'idx_games_scheduled');
SET @sql = IF(@idx_exists = 0, 'CREATE INDEX idx_games_scheduled ON games(scheduled_start_time, game_status)', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- Match results table
CREATE TABLE IF NOT EXISTS match_results (
match_result_id BIGINT AUTO_INCREMENT PRIMARY KEY,
game_id BIGINT NOT NULL,
valorant_match_id VARCHAR(255) NOT NULL,
map_name VARCHAR(50),
rounds_played INT NOT NULL,
winner_team_id BIGINT NOT NULL,
loser_team_id BIGINT NOT NULL,
winner_score INT NOT NULL,
loser_score INT NOT NULL,
game_started_at DATETIME NOT NULL,
game_duration INT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

UNIQUE INDEX idx_match_results_game (game_id),
INDEX idx_match_results_valorant (valorant_match_id),
CONSTRAINT fk_match_results_game FOREIGN KEY (game_id) REFERENCES games(game_id),
CONSTRAINT fk_match_results_winner FOREIGN KEY (winner_team_id) REFERENCES teams(team_id),
CONSTRAINT fk_match_results_loser FOREIGN KEY (loser_team_id) REFERENCES teams(team_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Match player stats table
CREATE TABLE IF NOT EXISTS match_player_stats (
match_player_stat_id BIGINT AUTO_INCREMENT PRIMARY KEY,
match_result_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
team_id BIGINT NOT NULL,
agent_name VARCHAR(50),
kills INT NOT NULL DEFAULT 0,
deaths INT NOT NULL DEFAULT 0,
assists INT NOT NULL DEFAULT 0,
score INT NOT NULL DEFAULT 0,
headshots INT NOT NULL DEFAULT 0,
bodyshots INT NOT NULL DEFAULT 0,
legshots INT NOT NULL DEFAULT 0,

INDEX idx_match_player_stats_result (match_result_id),
INDEX idx_match_player_stats_user (user_id),
CONSTRAINT fk_match_player_stats_result FOREIGN KEY (match_result_id) REFERENCES match_results(match_result_id),
CONSTRAINT fk_match_player_stats_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_match_player_stats_team FOREIGN KEY (team_id) REFERENCES teams(team_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
11 changes: 10 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ RUN go mod download
# Copy source code
COPY .. .

# Install golang-migrate CLI
RUN go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# Build the application from cmd/api
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w" \
Expand All @@ -30,7 +33,7 @@ RUN echo "=== Builder Stage File Structure ===" && \
FROM alpine:3.20

# 필수 패키지 설치
RUN apk --no-cache add ca-certificates tzdata wget curl
RUN apk --no-cache add ca-certificates tzdata wget curl make

# 비root 유저 생성
RUN addgroup -g 1000 appgroup && \
Expand All @@ -42,6 +45,12 @@ WORKDIR /app
# ⭐ 바이너리 복사
COPY --from=builder --chown=appuser:appgroup /app/server .

# ⭐ golang-migrate CLI 복사
COPY --from=builder /go/bin/migrate /usr/local/bin/migrate

# ⭐ Makefile 복사 (make migrate-up 등 실행용)
COPY --from=builder --chown=appuser:appgroup /app/Makefile .

# ⭐ db 디렉토리 복사 (migrations 포함)
COPY --from=builder --chown=appuser:appgroup /app/db ./db

Expand Down
Loading