feat: implement battle replay module with database migrations, repository, and CRUD service endpoints
All checks were successful
Build and Release / release (push) Successful in 1m32s

This commit is contained in:
2026-05-17 22:25:48 +07:00
parent 94601dbe58
commit 374c3b4f47
19 changed files with 10169 additions and 26 deletions

8902
FinalProject.drawio Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -98,6 +98,7 @@ func (s *FiberServer) SetupServer(
usageRepo := repositories.NewUsageRepository(redis) usageRepo := repositories.NewUsageRepository(redis)
statisticRepo := repositories.NewStatisticRepository(poolPg, redis) statisticRepo := repositories.NewStatisticRepository(poolPg, redis)
chatRepo := repositories.NewChatRepository(poolPg, redis) chatRepo := repositories.NewChatRepository(poolPg, redis)
battleReplayRepo := repositories.NewBattleReplayRepository(poolPg, redis)
// service setup // service setup
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis, poolPg) authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis, poolPg)
@@ -115,10 +116,11 @@ func (s *FiberServer) SetupServer(
submissionService := services.NewSubmissionService( submissionService := services.NewSubmissionService(
submissionRepo, projectRepo, commitRepo, submissionRepo, projectRepo, commitRepo,
userRepo, wikiRepo, geometryRepo, entityRepo, userRepo, wikiRepo, geometryRepo, entityRepo,
raguRepo, raguUtils, poolPg, redis, battleReplayRepo, raguRepo, raguUtils, poolPg, redis,
) )
chatbotService := services.NewChatbotService(raguRepo, usageRepo, chatRepo, raguUtils) chatbotService := services.NewChatbotService(raguRepo, usageRepo, chatRepo, raguUtils)
statisticService := services.NewStatisticService(statisticRepo) statisticService := services.NewStatisticService(statisticRepo)
battleReplayService := services.NewBattleReplayService(battleReplayRepo)
// controller setup // controller setup
authController := controllers.NewAuthController(authService, oauth) authController := controllers.NewAuthController(authService, oauth)
@@ -136,6 +138,7 @@ func (s *FiberServer) SetupServer(
submissionController := controllers.NewSubmissionController(submissionService) submissionController := controllers.NewSubmissionController(submissionService)
chatbotController := controllers.NewChatbotController(chatbotService) chatbotController := controllers.NewChatbotController(chatbotService)
statisticController := controllers.NewStatisticController(statisticService) statisticController := controllers.NewStatisticController(statisticService)
battleReplayController := controllers.NewBattleReplayController(battleReplayService)
// route setup // route setup
routes.AuthRoutes(s.App, authController, userRepo) routes.AuthRoutes(s.App, authController, userRepo)
@@ -152,5 +155,6 @@ func (s *FiberServer) SetupServer(
routes.SubmissionRoutes(s.App, submissionController, userRepo) routes.SubmissionRoutes(s.App, submissionController, userRepo)
routes.ChatbotRoutes(s.App, chatbotController, userRepo) routes.ChatbotRoutes(s.App, chatbotController, userRepo)
routes.StatisticRoutes(s.App, statisticController, userRepo) routes.StatisticRoutes(s.App, statisticController, userRepo)
routes.BattleReplayRoutes(s.App, battleReplayController)
routes.NotFoundRoute(s.App) routes.NotFoundRoute(s.App)
} }

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS battle_replays;

View File

@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS battle_replays (
id UUID PRIMARY KEY DEFAULT uuidv7(),
geometry_id UUID NOT NULL REFERENCES geometries(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
target_geometry_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_battle_replays_geometry_id ON battle_replays(geometry_id)
WHERE is_deleted = false;
CREATE INDEX idx_battle_replays_project_id ON battle_replays(project_id)
WHERE is_deleted = false;
CREATE INDEX idx_battle_replays_target_geometry_ids ON battle_replays USING GIN (target_geometry_ids)
WHERE is_deleted = false;
CREATE INDEX idx_battle_replays_updated_at ON battle_replays(updated_at DESC)
WHERE is_deleted = false;
DROP TRIGGER IF EXISTS trigger_battle_replays_updated_at ON battle_replays;
CREATE TRIGGER trigger_battle_replays_updated_at
BEFORE UPDATE ON battle_replays
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();

View File

@@ -0,0 +1,49 @@
-- name: CreateBattleReplay :one
INSERT INTO battle_replays (
id, geometry_id, project_id, target_geometry_ids, detail
) VALUES (
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4
)
RETURNING *;
-- name: GetBattleReplayById :one
SELECT *
FROM battle_replays
WHERE id = $1 AND is_deleted = false;
-- name: GetBattleReplaysByIDs :many
SELECT * FROM battle_replays WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: GetBattleReplaysByGeometryId :many
SELECT *
FROM battle_replays
WHERE geometry_id = $1 AND is_deleted = false;
-- name: GetBattleReplaysByGeometryIDs :many
SELECT *
FROM battle_replays
WHERE geometry_id = ANY($1::uuid[]) AND is_deleted = false;
-- name: GetBattleReplaysByProjectId :many
SELECT *
FROM battle_replays
WHERE project_id = $1 AND is_deleted = false;
-- name: UpdateBattleReplay :one
UPDATE battle_replays
SET
geometry_id = COALESCE(sqlc.narg('geometry_id'), geometry_id),
target_geometry_ids = COALESCE(sqlc.narg('target_geometry_ids'), target_geometry_ids),
detail = COALESCE(sqlc.narg('detail'), detail)
WHERE id = sqlc.arg('id') AND is_deleted = false
RETURNING *;
-- name: DeleteBattleReplay :exec
UPDATE battle_replays
SET is_deleted = true
WHERE id = $1;
-- name: DeleteBattleReplaysByIDs :exec
UPDATE battle_replays
SET is_deleted = true
WHERE id = ANY($1::uuid[]);

View File

@@ -243,3 +243,14 @@ CREATE TABLE IF NOT EXISTS chatbot_histories (
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE TABLE IF NOT EXISTS battle_replays (
id UUID PRIMARY KEY DEFAULT uuidv7(),
geometry_id UUID NOT NULL REFERENCES geometries(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
target_geometry_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

View File

@@ -399,6 +399,82 @@ const docTemplate = `{
} }
} }
}, },
"/battle-replays/geometry/{geometryId}": {
"get": {
"description": "Get all battle replays associated with a specific geometry",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"BattleReplays"
],
"summary": "Get battle replays by geometry ID",
"parameters": [
{
"type": "string",
"description": "Geometry ID",
"name": "geometryId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/battle-replays/{id}": {
"get": {
"description": "Get detailed information about a specific battle replay",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"BattleReplays"
],
"summary": "Get battle replay by ID",
"parameters": [
{
"type": "string",
"description": "Battle Replay ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/chatbot/chat": { "/chatbot/chat": {
"post": { "post": {
"security": [ "security": [
@@ -4241,6 +4317,33 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.BattleReplaySnapshot": {
"type": "object",
"required": [
"geometry_id",
"id"
],
"properties": {
"detail": {
"type": "array",
"items": {
"type": "integer"
}
},
"geometry_id": {
"type": "string"
},
"id": {
"type": "string"
},
"target_geometry_ids": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"history-api_internal_dtos_request.ChangeOwnerDto": { "history-api_internal_dtos_request.ChangeOwnerDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -4329,6 +4432,12 @@ const docTemplate = `{
"$ref": "#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot" "$ref": "#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot"
} }
}, },
"replays": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.BattleReplaySnapshot"
}
},
"wikis": { "wikis": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -392,6 +392,82 @@
} }
} }
}, },
"/battle-replays/geometry/{geometryId}": {
"get": {
"description": "Get all battle replays associated with a specific geometry",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"BattleReplays"
],
"summary": "Get battle replays by geometry ID",
"parameters": [
{
"type": "string",
"description": "Geometry ID",
"name": "geometryId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/battle-replays/{id}": {
"get": {
"description": "Get detailed information about a specific battle replay",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"BattleReplays"
],
"summary": "Get battle replay by ID",
"parameters": [
{
"type": "string",
"description": "Battle Replay ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/chatbot/chat": { "/chatbot/chat": {
"post": { "post": {
"security": [ "security": [
@@ -4234,6 +4310,33 @@
} }
} }
}, },
"history-api_internal_dtos_request.BattleReplaySnapshot": {
"type": "object",
"required": [
"geometry_id",
"id"
],
"properties": {
"detail": {
"type": "array",
"items": {
"type": "integer"
}
},
"geometry_id": {
"type": "string"
},
"id": {
"type": "string"
},
"target_geometry_ids": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"history-api_internal_dtos_request.ChangeOwnerDto": { "history-api_internal_dtos_request.ChangeOwnerDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -4322,6 +4425,12 @@
"$ref": "#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot" "$ref": "#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot"
} }
}, },
"replays": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.BattleReplaySnapshot"
}
},
"wikis": { "wikis": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -29,6 +29,24 @@ definitions:
- min_lat - min_lat
- min_lng - min_lng
type: object type: object
history-api_internal_dtos_request.BattleReplaySnapshot:
properties:
detail:
items:
type: integer
type: array
geometry_id:
type: string
id:
type: string
target_geometry_ids:
items:
type: string
type: array
required:
- geometry_id
- id
type: object
history-api_internal_dtos_request.ChangeOwnerDto: history-api_internal_dtos_request.ChangeOwnerDto:
properties: properties:
new_owner_id: new_owner_id:
@@ -88,6 +106,10 @@ definitions:
items: items:
$ref: '#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot' $ref: '#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot'
type: array type: array
replays:
items:
$ref: '#/definitions/history-api_internal_dtos_request.BattleReplaySnapshot'
type: array
wikis: wikis:
items: items:
$ref: '#/definitions/history-api_internal_dtos_request.WikiSnapshot' $ref: '#/definitions/history-api_internal_dtos_request.WikiSnapshot'
@@ -947,6 +969,56 @@ paths:
summary: Verify a security token summary: Verify a security token
tags: tags:
- Auth - Auth
/battle-replays/{id}:
get:
consumes:
- application/json
description: Get detailed information about a specific battle replay
parameters:
- description: Battle Replay ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
summary: Get battle replay by ID
tags:
- BattleReplays
/battle-replays/geometry/{geometryId}:
get:
consumes:
- application/json
description: Get all battle replays associated with a specific geometry
parameters:
- description: Geometry ID
in: path
name: geometryId
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
summary: Get battle replays by geometry ID
tags:
- BattleReplays
/chatbot/chat: /chatbot/chat:
post: post:
consumes: consumes:

View File

@@ -0,0 +1,72 @@
package controllers
import (
"context"
"history-api/internal/dtos/response"
"history-api/internal/services"
"time"
"github.com/gofiber/fiber/v3"
)
type BattleReplayController struct {
service services.BattleReplayService
}
func NewBattleReplayController(svc services.BattleReplayService) *BattleReplayController {
return &BattleReplayController{service: svc}
}
// GetBattleReplayById handles fetching a single battle replay by ID.
// @Summary Get battle replay by ID
// @Description Get detailed information about a specific battle replay
// @Tags BattleReplays
// @Accept json
// @Produce json
// @Param id path string true "Battle Replay ID"
// @Success 200 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Router /battle-replays/{id} [get]
func (h *BattleReplayController) GetBattleReplayById(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
res, err := h.service.GetByID(ctx, id)
if err != nil {
return c.Status(err.Code).JSON(response.CommonResponse{
Status: false,
Message: err.Message,
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// GetBattleReplaysByGeometryId handles fetching battle replays by geometry ID.
// @Summary Get battle replays by geometry ID
// @Description Get all battle replays associated with a specific geometry
// @Tags BattleReplays
// @Accept json
// @Produce json
// @Param geometryId path string true "Geometry ID"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Router /battle-replays/geometry/{geometryId} [get]
func (h *BattleReplayController) GetBattleReplaysByGeometryId(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
geometryID := c.Params("geometryId")
res, err := h.service.GetByGeometryID(ctx, geometryID)
if err != nil {
return c.Status(err.Code).JSON(response.CommonResponse{
Status: false,
Message: err.Message,
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -9,6 +9,14 @@ type CommitSnapshot struct {
Wikis []*WikiSnapshot `json:"wikis,omitempty" validate:"omitempty,dive"` Wikis []*WikiSnapshot `json:"wikis,omitempty" validate:"omitempty,dive"`
GeometryEntity []*GeometryEntitySnapshot `json:"geometry_entity,omitempty" validate:"omitempty,dive"` GeometryEntity []*GeometryEntitySnapshot `json:"geometry_entity,omitempty" validate:"omitempty,dive"`
EntityWiki []*EntityWikiLinkSnapshot `json:"entity_wiki,omitempty" validate:"omitempty,dive"` EntityWiki []*EntityWikiLinkSnapshot `json:"entity_wiki,omitempty" validate:"omitempty,dive"`
Replays []*BattleReplaySnapshot `json:"replays,omitempty" validate:"omitempty,dive"`
}
type BattleReplaySnapshot struct {
ID string `json:"id" validate:"required,uuidv7"`
GeometryID string `json:"geometry_id" validate:"required,uuidv7"`
TargetGeometryIDs []string `json:"target_geometry_ids,omitempty" validate:"omitempty,dive,uuidv7"`
Detail json.RawMessage `json:"detail,omitempty"`
} }
type FeatureCollection struct { type FeatureCollection struct {

View File

@@ -0,0 +1,17 @@
package response
import (
"encoding/json"
"time"
)
type BattleReplayResponse struct {
ID string `json:"id"`
GeometryID string `json:"geometry_id"`
ProjectID string `json:"project_id"`
TargetGeometryIDs json.RawMessage `json:"target_geometry_ids,omitempty"`
Detail json.RawMessage `json:"detail,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

View File

@@ -0,0 +1,272 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: battle_replay.sql
package sqlc
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
const createBattleReplay = `-- name: CreateBattleReplay :one
INSERT INTO battle_replays (
id, geometry_id, project_id, target_geometry_ids, detail
) VALUES (
COALESCE($5::uuid, uuidv7()), $1, $2, $3, $4
)
RETURNING id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
`
type CreateBattleReplayParams struct {
GeometryID pgtype.UUID `json:"geometry_id"`
ProjectID pgtype.UUID `json:"project_id"`
TargetGeometryIds json.RawMessage `json:"target_geometry_ids"`
Detail json.RawMessage `json:"detail"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) CreateBattleReplay(ctx context.Context, arg CreateBattleReplayParams) (BattleReplay, error) {
row := q.db.QueryRow(ctx, createBattleReplay,
arg.GeometryID,
arg.ProjectID,
arg.TargetGeometryIds,
arg.Detail,
arg.ID,
)
var i BattleReplay
err := row.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteBattleReplay = `-- name: DeleteBattleReplay :exec
UPDATE battle_replays
SET is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteBattleReplay(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteBattleReplay, id)
return err
}
const deleteBattleReplaysByIDs = `-- name: DeleteBattleReplaysByIDs :exec
UPDATE battle_replays
SET is_deleted = true
WHERE id = ANY($1::uuid[])
`
func (q *Queries) DeleteBattleReplaysByIDs(ctx context.Context, dollar_1 []pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteBattleReplaysByIDs, dollar_1)
return err
}
const getBattleReplayById = `-- name: GetBattleReplayById :one
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
FROM battle_replays
WHERE id = $1 AND is_deleted = false
`
func (q *Queries) GetBattleReplayById(ctx context.Context, id pgtype.UUID) (BattleReplay, error) {
row := q.db.QueryRow(ctx, getBattleReplayById, id)
var i BattleReplay
err := row.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getBattleReplaysByGeometryIDs = `-- name: GetBattleReplaysByGeometryIDs :many
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
FROM battle_replays
WHERE geometry_id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetBattleReplaysByGeometryIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]BattleReplay, error) {
rows, err := q.db.Query(ctx, getBattleReplaysByGeometryIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []BattleReplay{}
for rows.Next() {
var i BattleReplay
if err := rows.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBattleReplaysByGeometryId = `-- name: GetBattleReplaysByGeometryId :many
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
FROM battle_replays
WHERE geometry_id = $1 AND is_deleted = false
`
func (q *Queries) GetBattleReplaysByGeometryId(ctx context.Context, geometryID pgtype.UUID) ([]BattleReplay, error) {
rows, err := q.db.Query(ctx, getBattleReplaysByGeometryId, geometryID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []BattleReplay{}
for rows.Next() {
var i BattleReplay
if err := rows.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBattleReplaysByIDs = `-- name: GetBattleReplaysByIDs :many
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at FROM battle_replays WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetBattleReplaysByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]BattleReplay, error) {
rows, err := q.db.Query(ctx, getBattleReplaysByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []BattleReplay{}
for rows.Next() {
var i BattleReplay
if err := rows.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBattleReplaysByProjectId = `-- name: GetBattleReplaysByProjectId :many
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
FROM battle_replays
WHERE project_id = $1 AND is_deleted = false
`
func (q *Queries) GetBattleReplaysByProjectId(ctx context.Context, projectID pgtype.UUID) ([]BattleReplay, error) {
rows, err := q.db.Query(ctx, getBattleReplaysByProjectId, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []BattleReplay{}
for rows.Next() {
var i BattleReplay
if err := rows.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateBattleReplay = `-- name: UpdateBattleReplay :one
UPDATE battle_replays
SET
geometry_id = COALESCE($1, geometry_id),
target_geometry_ids = COALESCE($2, target_geometry_ids),
detail = COALESCE($3, detail)
WHERE id = $4 AND is_deleted = false
RETURNING id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
`
type UpdateBattleReplayParams struct {
GeometryID pgtype.UUID `json:"geometry_id"`
TargetGeometryIds []byte `json:"target_geometry_ids"`
Detail []byte `json:"detail"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) UpdateBattleReplay(ctx context.Context, arg UpdateBattleReplayParams) (BattleReplay, error) {
row := q.db.QueryRow(ctx, updateBattleReplay,
arg.GeometryID,
arg.TargetGeometryIds,
arg.Detail,
arg.ID,
)
var i BattleReplay
err := row.Scan(
&i.ID,
&i.GeometryID,
&i.ProjectID,
&i.TargetGeometryIds,
&i.Detail,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@@ -11,6 +11,17 @@ import (
"github.com/pgvector/pgvector-go" "github.com/pgvector/pgvector-go"
) )
type BattleReplay struct {
ID pgtype.UUID `json:"id"`
GeometryID pgtype.UUID `json:"geometry_id"`
ProjectID pgtype.UUID `json:"project_id"`
TargetGeometryIds json.RawMessage `json:"target_geometry_ids"`
Detail json.RawMessage `json:"detail"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type ChatbotHistory struct { type ChatbotHistory struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"` UserID pgtype.UUID `json:"user_id"`

View File

@@ -0,0 +1,48 @@
package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"time"
)
type BattleReplayEntity struct {
ID string `json:"id"`
GeometryID string `json:"geometry_id"`
ProjectID string `json:"project_id"`
TargetGeometryIDs json.RawMessage `json:"target_geometry_ids"`
Detail json.RawMessage `json:"detail"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (b *BattleReplayEntity) ToResponse() *response.BattleReplayResponse {
if b == nil {
return nil
}
return &response.BattleReplayResponse{
ID: b.ID,
GeometryID: b.GeometryID,
ProjectID: b.ProjectID,
TargetGeometryIDs: b.TargetGeometryIDs,
Detail: b.Detail,
IsDeleted: b.IsDeleted,
CreatedAt: b.CreatedAt,
UpdatedAt: b.UpdatedAt,
}
}
func BattleReplaysEntityToResponse(bs []*BattleReplayEntity) []*response.BattleReplayResponse {
out := make([]*response.BattleReplayResponse, 0)
if bs == nil {
return out
}
for _, b := range bs {
if b == nil {
continue
}
out = append(out, b.ToResponse())
}
return out
}

View File

@@ -0,0 +1,290 @@
package repositories
import (
"context"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type BattleReplayRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.BattleReplayEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.BattleReplayEntity, error)
GetByGeometryID(ctx context.Context, geometryID pgtype.UUID) ([]*models.BattleReplayEntity, error)
GetByGeometryIDs(ctx context.Context, geometryIDs []string) ([]*models.BattleReplayEntity, error)
Create(ctx context.Context, params sqlc.CreateBattleReplayParams) (*models.BattleReplayEntity, error)
Update(ctx context.Context, params sqlc.UpdateBattleReplayParams) (*models.BattleReplayEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.BattleReplayEntity, error)
WithTx(tx pgx.Tx) BattleReplayRepository
}
type battleReplayRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewBattleReplayRepository(db sqlc.DBTX, c cache.Cache) BattleReplayRepository {
return &battleReplayRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *battleReplayRepository) WithTx(tx pgx.Tx) BattleReplayRepository {
return &battleReplayRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *battleReplayRepository) rowToEntity(row sqlc.BattleReplay) *models.BattleReplayEntity {
return &models.BattleReplayEntity{
ID: convert.UUIDToString(row.ID),
GeometryID: convert.UUIDToString(row.GeometryID),
ProjectID: convert.UUIDToString(row.ProjectID),
TargetGeometryIDs: row.TargetGeometryIds,
Detail: row.Detail,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
}
func (r *battleReplayRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.BattleReplayEntity, error) {
if len(ids) == 0 {
return []*models.BattleReplayEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("battle_replay:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var items []*models.BattleReplayEntity
missingToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
}
}
dbMap := make(map[string]*models.BattleReplayEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetBattleReplaysByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := r.rowToEntity(row)
dbMap[item.ID] = item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.BattleReplayEntity
if err := json.Unmarshal(b, &u); err == nil {
items = append(items, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
items = append(items, item)
missingToCache[keys[i]] = item
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return items, nil
}
func (r *battleReplayRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.BattleReplayEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *battleReplayRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.BattleReplayEntity, error) {
cacheId := fmt.Sprintf("battle_replay:id:%s", convert.UUIDToString(id))
var item models.BattleReplayEntity
err := r.c.Get(ctx, cacheId, &item)
if err == nil {
_ = r.c.Set(ctx, cacheId, item, constants.NormalCacheDuration)
return &item, nil
}
row, err := r.q.GetBattleReplayById(ctx, id)
if err != nil {
return nil, err
}
entity := r.rowToEntity(row)
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
return entity, nil
}
func (r *battleReplayRepository) GetByGeometryID(ctx context.Context, geometryID pgtype.UUID) ([]*models.BattleReplayEntity, error) {
cacheKey := fmt.Sprintf("battle_replay:geometry:%s", convert.UUIDToString(geometryID))
var cachedIDs []string
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetBattleReplaysByGeometryId(ctx, geometryID)
if err != nil {
return nil, err
}
var items []*models.BattleReplayEntity
var ids []string
itemToCache := make(map[string]any)
for _, row := range rows {
item := r.rowToEntity(row)
ids = append(ids, item.ID)
items = append(items, item)
itemToCache[fmt.Sprintf("battle_replay:id:%s", item.ID)] = item
}
if len(itemToCache) > 0 {
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
}
return items, nil
}
func (r *battleReplayRepository) GetByGeometryIDs(ctx context.Context, geometryIDs []string) ([]*models.BattleReplayEntity, error) {
if len(geometryIDs) == 0 {
return []*models.BattleReplayEntity{}, nil
}
var pgIds []pgtype.UUID
for _, id := range geometryIDs {
pgId := pgtype.UUID{}
if err := pgId.Scan(id); err == nil {
pgIds = append(pgIds, pgId)
}
}
rows, err := r.q.GetBattleReplaysByGeometryIDs(ctx, pgIds)
if err != nil {
return nil, err
}
var items []*models.BattleReplayEntity
itemToCache := make(map[string]any)
for _, row := range rows {
item := r.rowToEntity(row)
items = append(items, item)
itemToCache[fmt.Sprintf("battle_replay:id:%s", item.ID)] = item
}
if len(itemToCache) > 0 {
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
}
return items, nil
}
func (r *battleReplayRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.BattleReplayEntity, error) {
cacheKey := fmt.Sprintf("battle_replay:project:%s", convert.UUIDToString(projectID))
var cachedIDs []string
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetBattleReplaysByProjectId(ctx, projectID)
if err != nil {
return nil, err
}
var items []*models.BattleReplayEntity
var ids []string
itemToCache := make(map[string]any)
for _, row := range rows {
item := r.rowToEntity(row)
ids = append(ids, item.ID)
items = append(items, item)
itemToCache[fmt.Sprintf("battle_replay:id:%s", item.ID)] = item
}
if len(itemToCache) > 0 {
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
}
return items, nil
}
func (r *battleReplayRepository) Create(ctx context.Context, params sqlc.CreateBattleReplayParams) (*models.BattleReplayEntity, error) {
row, err := r.q.CreateBattleReplay(ctx, params)
if err != nil {
return nil, err
}
entity := r.rowToEntity(row)
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:project:%s", entity.ProjectID))
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:geometry:%s", entity.GeometryID))
return entity, nil
}
func (r *battleReplayRepository) Update(ctx context.Context, params sqlc.UpdateBattleReplayParams) (*models.BattleReplayEntity, error) {
row, err := r.q.UpdateBattleReplay(ctx, params)
if err != nil {
return nil, err
}
entity := r.rowToEntity(row)
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:id:%s", entity.ID))
return entity, nil
}
func (r *battleReplayRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteBattleReplay(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:id:%s", convert.UUIDToString(id)))
return nil
}
func (r *battleReplayRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
err := r.q.DeleteBattleReplaysByIDs(ctx, ids)
if err != nil {
return err
}
if len(ids) > 0 {
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("battle_replay:id:%s", convert.UUIDToString(id))
}
_ = r.c.Del(ctx, keys...)
}
return nil
}

View File

@@ -0,0 +1,13 @@
package routes
import (
"history-api/internal/controllers"
"github.com/gofiber/fiber/v3"
)
func BattleReplayRoutes(router fiber.Router, battleReplayController *controllers.BattleReplayController) {
br := router.Group("/battle-replays")
br.Get("/geometry/:geometryId", battleReplayController.GetBattleReplaysByGeometryId)
br.Get("/:id", battleReplayController.GetBattleReplayById)
}

View File

@@ -0,0 +1,54 @@
package services
import (
"context"
"history-api/internal/dtos/response"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/convert"
"github.com/gofiber/fiber/v3"
)
type BattleReplayService interface {
GetByID(ctx context.Context, id string) (*response.BattleReplayResponse, *fiber.Error)
GetByGeometryID(ctx context.Context, geometryID string) ([]*response.BattleReplayResponse, *fiber.Error)
}
type battleReplayService struct {
battleReplayRepo repositories.BattleReplayRepository
}
func NewBattleReplayService(battleReplayRepo repositories.BattleReplayRepository) BattleReplayService {
return &battleReplayService{
battleReplayRepo: battleReplayRepo,
}
}
func (s *battleReplayService) GetByID(ctx context.Context, id string) (*response.BattleReplayResponse, *fiber.Error) {
replayUUID, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid battle replay ID format")
}
replay, err := s.battleReplayRepo.GetByID(ctx, replayUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Battle replay not found")
}
return replay.ToResponse(), nil
}
func (s *battleReplayService) GetByGeometryID(ctx context.Context, geometryID string) ([]*response.BattleReplayResponse, *fiber.Error) {
geomUUID, err := convert.StringToUUID(geometryID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid geometry ID format")
}
replays, err := s.battleReplayRepo.GetByGeometryID(ctx, geomUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get battle replays")
}
return models.BattleReplaysEntityToResponse(replays), nil
}

View File

@@ -32,17 +32,18 @@ type SubmissionService interface {
} }
type submissionService struct { type submissionService struct {
submissionRepo repositories.SubmissionRepository submissionRepo repositories.SubmissionRepository
projectRepo repositories.ProjectRepository projectRepo repositories.ProjectRepository
commitRepo repositories.CommitRepository commitRepo repositories.CommitRepository
userRepo repositories.UserRepository userRepo repositories.UserRepository
wikiRepo repositories.WikiRepository wikiRepo repositories.WikiRepository
geometryRepo repositories.GeometryRepository geometryRepo repositories.GeometryRepository
entityRepo repositories.EntityRepository entityRepo repositories.EntityRepository
ragRepo repositories.RagRepository battleReplayRepo repositories.BattleReplayRepository
ragUtils *ai.RagUtils ragRepo repositories.RagRepository
db *pgxpool.Pool ragUtils *ai.RagUtils
c cache.Cache db *pgxpool.Pool
c cache.Cache
} }
func NewSubmissionService( func NewSubmissionService(
@@ -53,23 +54,25 @@ func NewSubmissionService(
wikiRepo repositories.WikiRepository, wikiRepo repositories.WikiRepository,
geometryRepo repositories.GeometryRepository, geometryRepo repositories.GeometryRepository,
entityRepo repositories.EntityRepository, entityRepo repositories.EntityRepository,
battleReplayRepo repositories.BattleReplayRepository,
ragRepo repositories.RagRepository, ragRepo repositories.RagRepository,
ragUtils *ai.RagUtils, ragUtils *ai.RagUtils,
db *pgxpool.Pool, db *pgxpool.Pool,
c cache.Cache, c cache.Cache,
) SubmissionService { ) SubmissionService {
return &submissionService{ return &submissionService{
submissionRepo: submissionRepo, submissionRepo: submissionRepo,
projectRepo: projectRepo, projectRepo: projectRepo,
commitRepo: commitRepo, commitRepo: commitRepo,
userRepo: userRepo, userRepo: userRepo,
wikiRepo: wikiRepo, wikiRepo: wikiRepo,
geometryRepo: geometryRepo, geometryRepo: geometryRepo,
entityRepo: entityRepo, entityRepo: entityRepo,
ragRepo: ragRepo, battleReplayRepo: battleReplayRepo,
ragUtils: ragUtils, ragRepo: ragRepo,
db: db, ragUtils: ragUtils,
c: c, db: db,
c: c,
} }
} }
@@ -187,6 +190,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
entityRepo := s.entityRepo.WithTx(tx) entityRepo := s.entityRepo.WithTx(tx)
geometryRepo := s.geometryRepo.WithTx(tx) geometryRepo := s.geometryRepo.WithTx(tx)
wikiRepo := s.wikiRepo.WithTx(tx) wikiRepo := s.wikiRepo.WithTx(tx)
battleReplayRepo := s.battleReplayRepo.WithTx(tx)
submissionUUID, err := convert.StringToUUID(submissionID) submissionUUID, err := convert.StringToUUID(submissionID)
if err != nil { if err != nil {
@@ -229,6 +233,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
listDeleteEntities := make([]pgtype.UUID, 0) listDeleteEntities := make([]pgtype.UUID, 0)
listDeleteWikis := make([]pgtype.UUID, 0) listDeleteWikis := make([]pgtype.UUID, 0)
listDeleteGeometries := make([]pgtype.UUID, 0) listDeleteGeometries := make([]pgtype.UUID, 0)
listDeleteBattleReplays := make([]pgtype.UUID, 0)
var snapshotData request.CommitSnapshot var snapshotData request.CommitSnapshot
err = json.Unmarshal(commit.SnapshotJson, &snapshotData) err = json.Unmarshal(commit.SnapshotJson, &snapshotData)
if err != nil { if err != nil {
@@ -242,17 +247,22 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
} }
currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID) currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found") return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found: "+err.Error())
} }
currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID) currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found") return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found: "+err.Error())
} }
currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID) currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found") return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found: "+err.Error())
}
currentBattleReplay, err := s.battleReplayRepo.GetByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Battle replay not found: "+err.Error())
} }
persistItemIDs := make(map[string]struct{}) persistItemIDs := make(map[string]struct{})
@@ -265,6 +275,9 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
for _, item := range snapshotData.Wikis { for _, item := range snapshotData.Wikis {
persistItemIDs[item.ID] = struct{}{} persistItemIDs[item.ID] = struct{}{}
} }
for _, item := range snapshotData.Replays {
persistItemIDs[item.ID] = struct{}{}
}
persistCurrentItemIDs := make(map[string]struct{}) persistCurrentItemIDs := make(map[string]struct{})
for _, item := range currentEntity { for _, item := range currentEntity {
@@ -276,6 +289,9 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
for _, item := range currentWiki { for _, item := range currentWiki {
persistCurrentItemIDs[item.ID] = struct{}{} persistCurrentItemIDs[item.ID] = struct{}{}
} }
for _, item := range currentBattleReplay {
persistCurrentItemIDs[item.ID] = struct{}{}
}
for _, e := range currentEntity { for _, e := range currentEntity {
if _, ok := persistItemIDs[e.ID]; !ok { if _, ok := persistItemIDs[e.ID]; !ok {
@@ -310,6 +326,17 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
} }
} }
for _, br := range currentBattleReplay {
if _, ok := persistItemIDs[br.ID]; !ok {
itemUUID, err := convert.StringToUUID(br.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID")
}
listDeleteBattleReplays = append(listDeleteBattleReplays, itemUUID)
delete(persistCurrentItemIDs, br.ID)
}
}
if len(listDeleteEntities) > 0 { if len(listDeleteEntities) > 0 {
if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil { if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities")
@@ -328,6 +355,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
} }
} }
if len(listDeleteBattleReplays) > 0 {
if err = battleReplayRepo.DeleteByIDs(ctx, listDeleteBattleReplays); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete battle replays")
}
}
refEntityIDs := []string{} refEntityIDs := []string{}
for _, e := range snapshotData.Entities { for _, e := range snapshotData.Entities {
if e.Source == "ref" { if e.Source == "ref" {
@@ -570,6 +603,46 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
} }
snapshotData.Wikis = newWikis snapshotData.Wikis = newWikis
for _, replay := range snapshotData.Replays {
replayUUID, err := convert.StringToUUID(replay.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID")
}
geomUUID, err := convert.StringToUUID(replay.GeometryID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID in battle replay")
}
targetIDs, err := json.Marshal(replay.TargetGeometryIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal target geometry IDs")
}
if _, ok := persistCurrentItemIDs[replay.ID]; ok {
_, err := battleReplayRepo.Update(ctx, sqlc.UpdateBattleReplayParams{
ID: replayUUID,
GeometryID: geomUUID,
TargetGeometryIds: targetIDs,
Detail: replay.Detail,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update battle replay: "+err.Error())
}
} else {
_, err := battleReplayRepo.Create(ctx, sqlc.CreateBattleReplayParams{
ID: replayUUID,
GeometryID: geomUUID,
ProjectID: projectUUID,
TargetGeometryIds: targetIDs,
Detail: replay.Detail,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create battle replay: "+err.Error())
}
}
}
err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID) err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error()) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error())