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

@@ -39,6 +39,7 @@ type submissionService struct {
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
@@ -53,6 +54,7 @@ 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,
@@ -66,6 +68,7 @@ func NewSubmissionService(
wikiRepo: wikiRepo, wikiRepo: wikiRepo,
geometryRepo: geometryRepo, geometryRepo: geometryRepo,
entityRepo: entityRepo, entityRepo: entityRepo,
battleReplayRepo: battleReplayRepo,
ragRepo: ragRepo, ragRepo: ragRepo,
ragUtils: ragUtils, ragUtils: ragUtils,
db: db, db: db,
@@ -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())