From cc5b18ab91222bf08d55719fc6cf65f6559be754 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 21 Jun 2026 09:46:05 +0800 Subject: [PATCH 1/6] refactor(project-listing): move cursor pagination helpers Project list pagination helpers lived in the root service package alongside higher-level list orchestration. Move cursor parsing, cursor encoding, page normalization, and pagination response shaping into the listing package. The root project service keeps the same list methods while delegating low-level pagination details. --- backend/internal/services/project/list.go | 43 ++----- .../internal/services/project/list_cursor.go | 94 -------------- .../services/project/listing/cursor.go | 120 ++++++++++++++++++ 3 files changed, 129 insertions(+), 128 deletions(-) delete mode 100644 backend/internal/services/project/list_cursor.go create mode 100644 backend/internal/services/project/listing/cursor.go diff --git a/backend/internal/services/project/list.go b/backend/internal/services/project/list.go index 59ea1722..657f5f83 100644 --- a/backend/internal/services/project/list.go +++ b/backend/internal/services/project/list.go @@ -1,13 +1,12 @@ package project import ( - "strings" - "github.com/google/uuid" "gorm.io/gorm" "github.com/kurodakayn/mpp-backend/internal/dto" "github.com/kurodakayn/mpp-backend/internal/models" + projectlisting "github.com/kurodakayn/mpp-backend/internal/services/project/listing" projectpresenter "github.com/kurodakayn/mpp-backend/internal/services/project/presenter" ) @@ -120,8 +119,8 @@ func (s *Service) projectListReadDB(scopeUserID *uuid.UUID) *gorm.DB { func (s *Service) ListProjectPage(query *gorm.DB, cursor string, page, limit int, scopeUserID *uuid.UUID) (*dto.PaginationResponse, error) { var projects []models.Project - page, limit = normalizeProjectListPage(page, limit) - query, err := applyProjectListCursor(query, cursor) + page, limit = projectlisting.NormalizePage(page, limit) + query, err := projectlisting.ApplyCursor(query, cursor) if err != nil { return nil, err } @@ -166,17 +165,17 @@ func (s *Service) ListProjectPage(query *gorm.DB, cursor string, page, limit int nextCursor := "" if hasMore && len(projects) > 0 { - nextCursor = encodeProjectListCursor(projects[len(projects)-1]) + nextCursor = projectlisting.EncodeCursor(projects[len(projects)-1]) } - return projectPaginationResponse(items, cursor, page, limit, hasMore, nextCursor), nil + return projectlisting.PaginationResponse(items, cursor, page, limit, hasMore, nextCursor), nil } func (s *Service) ListProjectSummaryPage(query *gorm.DB, cursor string, page, limit int) (*dto.PaginationResponse, error) { var summaries []models.ProjectListSummary - page, limit = normalizeProjectListPage(page, limit) - query, err := applyProjectListCursorColumns(query, cursor, "project_list_summaries.created_at", "project_list_summaries.project_id") + page, limit = projectlisting.NormalizePage(page, limit) + query, err := projectlisting.ApplyCursorColumns(query, cursor, "project_list_summaries.created_at", "project_list_summaries.project_id") if err != nil { return nil, err } @@ -206,32 +205,8 @@ func (s *Service) ListProjectSummaryPage(query *gorm.DB, cursor string, page, li nextCursor := "" if hasMore && len(summaries) > 0 { last := summaries[len(summaries)-1] - nextCursor = encodeProjectListCursorValues(last.CreatedAt, last.ProjectID) - } - - return projectPaginationResponse(items, cursor, page, limit, hasMore, nextCursor), nil -} - -func projectPaginationResponse(items []dto.ProjectListItem, cursor string, page int, limit int, hasMore bool, nextCursor string) *dto.PaginationResponse { - total := int64((page-1)*limit + len(items)) - if hasMore { - total++ - } - totalPages := page - if len(items) == 0 && page == 1 { - totalPages = 0 - } else if hasMore { - totalPages = page + 1 + nextCursor = projectlisting.EncodeCursorValues(last.CreatedAt, last.ProjectID) } - return &dto.PaginationResponse{ - Items: items, - Page: page, - Limit: limit, - Total: total, - TotalPages: totalPages, - Cursor: strings.TrimSpace(cursor), - NextCursor: nextCursor, - HasMore: hasMore, - } + return projectlisting.PaginationResponse(items, cursor, page, limit, hasMore, nextCursor), nil } diff --git a/backend/internal/services/project/list_cursor.go b/backend/internal/services/project/list_cursor.go deleted file mode 100644 index 63db5efb..00000000 --- a/backend/internal/services/project/list_cursor.go +++ /dev/null @@ -1,94 +0,0 @@ -package project - -import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "strings" - "time" - - "github.com/google/uuid" - "gorm.io/gorm" - - "github.com/kurodakayn/mpp-backend/internal/models" -) - -const ( - defaultProjectListLimit = 10 - maxProjectListLimit = 100 -) - -var errEmptyProjectListCursor = errors.New("empty project list cursor") - -type projectListCursor struct { - CreatedAt time.Time `json:"created_at"` - ID uuid.UUID `json:"id"` -} - -func normalizeProjectListPage(page, limit int) (int, int) { - if page < 1 { - page = 1 - } - if limit < 1 { - limit = defaultProjectListLimit - } - if limit > maxProjectListLimit { - limit = maxProjectListLimit - } - return page, limit -} - -func applyProjectListCursor(query *gorm.DB, cursor string) (*gorm.DB, error) { - return applyProjectListCursorColumns(query, cursor, "projects.created_at", "projects.id") -} - -func applyProjectListCursorColumns(query *gorm.DB, cursor, createdAtColumn, idColumn string) (*gorm.DB, error) { - if strings.TrimSpace(cursor) == "" { - return query, nil - } - decoded, err := decodeProjectListCursor(cursor) - if err != nil { - return nil, err - } - return query.Where( - fmt.Sprintf("(%s < ? OR (%s = ? AND %s > ?))", createdAtColumn, createdAtColumn, idColumn), - decoded.CreatedAt, - decoded.CreatedAt, - decoded.ID, - ), nil -} - -func decodeProjectListCursor(cursor string) (*projectListCursor, error) { - cursor = strings.TrimSpace(cursor) - if cursor == "" { - return nil, errEmptyProjectListCursor - } - raw, err := base64.RawURLEncoding.DecodeString(cursor) - if err != nil { - return nil, fmt.Errorf("%w: invalid project list cursor", ErrInvalidProject) - } - var decoded projectListCursor - if err := json.Unmarshal(raw, &decoded); err != nil { - return nil, fmt.Errorf("%w: invalid project list cursor", ErrInvalidProject) - } - if decoded.ID == uuid.Nil || decoded.CreatedAt.IsZero() { - return nil, fmt.Errorf("%w: invalid project list cursor", ErrInvalidProject) - } - return &decoded, nil -} - -func encodeProjectListCursor(project models.Project) string { - return encodeProjectListCursorValues(project.CreatedAt, project.ID) -} - -func encodeProjectListCursorValues(createdAt time.Time, id uuid.UUID) string { - encoded, err := json.Marshal(projectListCursor{ - CreatedAt: createdAt, - ID: id, - }) - if err != nil { - return "" - } - return base64.RawURLEncoding.EncodeToString(encoded) -} diff --git a/backend/internal/services/project/listing/cursor.go b/backend/internal/services/project/listing/cursor.go new file mode 100644 index 00000000..acdf4b6a --- /dev/null +++ b/backend/internal/services/project/listing/cursor.go @@ -0,0 +1,120 @@ +package listing + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" + + "github.com/kurodakayn/mpp-backend/internal/dto" + "github.com/kurodakayn/mpp-backend/internal/models" + "github.com/kurodakayn/mpp-backend/internal/services/project/projecterr" +) + +const ( + defaultProjectListLimit = 10 + maxProjectListLimit = 100 +) + +var errEmptyCursor = errors.New("empty project list cursor") + +type cursorPayload struct { + CreatedAt time.Time `json:"created_at"` + ID uuid.UUID `json:"id"` +} + +func NormalizePage(page, limit int) (int, int) { + if page < 1 { + page = 1 + } + if limit < 1 { + limit = defaultProjectListLimit + } + if limit > maxProjectListLimit { + limit = maxProjectListLimit + } + return page, limit +} + +func ApplyCursor(query *gorm.DB, cursor string) (*gorm.DB, error) { + return ApplyCursorColumns(query, cursor, "projects.created_at", "projects.id") +} + +func ApplyCursorColumns(query *gorm.DB, cursor, createdAtColumn, idColumn string) (*gorm.DB, error) { + if strings.TrimSpace(cursor) == "" { + return query, nil + } + decoded, err := decodeCursor(cursor) + if err != nil { + return nil, err + } + return query.Where( + fmt.Sprintf("(%s < ? OR (%s = ? AND %s > ?))", createdAtColumn, createdAtColumn, idColumn), + decoded.CreatedAt, + decoded.CreatedAt, + decoded.ID, + ), nil +} + +func EncodeCursor(project models.Project) string { + return EncodeCursorValues(project.CreatedAt, project.ID) +} + +func EncodeCursorValues(createdAt time.Time, id uuid.UUID) string { + encoded, err := json.Marshal(cursorPayload{ + CreatedAt: createdAt, + ID: id, + }) + if err != nil { + return "" + } + return base64.RawURLEncoding.EncodeToString(encoded) +} + +func PaginationResponse(items []dto.ProjectListItem, cursor string, page int, limit int, hasMore bool, nextCursor string) *dto.PaginationResponse { + total := int64((page-1)*limit + len(items)) + if hasMore { + total++ + } + totalPages := page + if len(items) == 0 && page == 1 { + totalPages = 0 + } else if hasMore { + totalPages = page + 1 + } + + return &dto.PaginationResponse{ + Items: items, + Page: page, + Limit: limit, + Total: total, + TotalPages: totalPages, + Cursor: strings.TrimSpace(cursor), + NextCursor: nextCursor, + HasMore: hasMore, + } +} + +func decodeCursor(cursor string) (*cursorPayload, error) { + cursor = strings.TrimSpace(cursor) + if cursor == "" { + return nil, errEmptyCursor + } + raw, err := base64.RawURLEncoding.DecodeString(cursor) + if err != nil { + return nil, fmt.Errorf("%w: invalid project list cursor", projecterr.ErrInvalidProject) + } + var decoded cursorPayload + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("%w: invalid project list cursor", projecterr.ErrInvalidProject) + } + if decoded.ID == uuid.Nil || decoded.CreatedAt.IsZero() { + return nil, fmt.Errorf("%w: invalid project list cursor", projecterr.ErrInvalidProject) + } + return &decoded, nil +} From 2674ebab494cc6035aa8368f8096d17e809ce159 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 21 Jun 2026 09:46:38 +0800 Subject: [PATCH 2/6] refactor(project-mediausage): move media usage tracking Media reference extraction and asset usage writes were mixed into the root project service package. Move the media usage implementation and its duplicate-resource test into a focused mediausage package. The project service keeps its existing refresh hooks while delegating the storage details. --- .../internal/services/project/media_usage.go | 87 +------------- .../project/mediausage/media_usage.go | 106 ++++++++++++++++++ .../{ => mediausage}/media_usage_test.go | 8 +- 3 files changed, 113 insertions(+), 88 deletions(-) create mode 100644 backend/internal/services/project/mediausage/media_usage.go rename backend/internal/services/project/{ => mediausage}/media_usage_test.go (79%) diff --git a/backend/internal/services/project/media_usage.go b/backend/internal/services/project/media_usage.go index 99079c93..afcf9151 100644 --- a/backend/internal/services/project/media_usage.go +++ b/backend/internal/services/project/media_usage.go @@ -1,52 +1,19 @@ package project import ( - "regexp" - "strings" - "github.com/google/uuid" "gorm.io/gorm" - "gorm.io/gorm/clause" "github.com/kurodakayn/mpp-backend/internal/models" + projectmediausage "github.com/kurodakayn/mpp-backend/internal/services/project/mediausage" ) -const mediaObjectRefPrefix = "mpp://media/" - -var mediaObjectRefPattern = regexp.MustCompile(`mpp://media/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})`) - func refreshProjectMediaUsages(tx *gorm.DB, project models.Project, publications []models.ProjectPlatformPublication) error { - workspaceID := projectWorkspaceID(project) - if err := tx.Where("project_id = ?", project.ID).Delete(&models.MediaAssetUsage{}).Error; err != nil { - return err - } - - sourceRefs := collectMediaAssetIDs(project.SourceContent) - if err := upsertMediaUsages(tx, workspaceID, &project.ID, nil, nil, "project", project.ID, models.MediaAssetUsageEditorImage, sourceRefs); err != nil { - return err - } - for _, publication := range publications { - refs := collectMediaAssetIDs(string(publication.Config), string(publication.AdaptedContent)) - if len(refs) == 0 { - continue - } - if err := upsertMediaUsages(tx, workspaceID, &project.ID, &publication.ID, nil, "publication", publication.ID, publication.Platform, refs); err != nil { - return err - } - } - return nil + return projectmediausage.RefreshProject(tx, project, publications) } func refreshContentTemplateMediaUsages(tx *gorm.DB, workspaceID uuid.UUID, template models.ContentTemplate) error { - if template.ID == uuid.Nil { - return ErrInvalidProject - } - if err := tx.Where("template_id = ?", template.ID).Delete(&models.MediaAssetUsage{}).Error; err != nil { - return err - } - - refs := collectMediaAssetIDs(template.SourceTemplate, string(template.PlatformConfig)) - return upsertMediaUsages(tx, workspaceID, nil, nil, &template.ID, "template", template.ID, models.MediaAssetUsageEditorImage, refs) + return projectmediausage.RefreshContentTemplate(tx, workspaceID, template) } func (s *Service) RefreshProjectMediaUsages(projectID uuid.UUID) error { @@ -65,51 +32,3 @@ func (s *Service) RefreshProjectMediaUsages(projectID uuid.UUID) error { return refreshProjectMediaUsages(tx, project, publications) }) } - -func upsertMediaUsages(tx *gorm.DB, workspaceID uuid.UUID, projectID *uuid.UUID, publicationID *uuid.UUID, templateID *uuid.UUID, resourceType string, resourceID uuid.UUID, usageKind string, assetIDs []uuid.UUID) error { - for _, assetID := range assetIDs { - usage := models.MediaAssetUsage{ - MediaAssetID: assetID, - WorkspaceID: workspaceID, - ProjectID: projectID, - PublicationID: publicationID, - TemplateID: templateID, - ResourceType: resourceType, - ResourceID: resourceID, - UsageKind: usageKind, - } - if err := tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{ - {Name: "media_asset_id"}, - {Name: "resource_type"}, - {Name: "resource_id"}, - }, - DoNothing: true, - }).Create(&usage).Error; err != nil { - return err - } - } - return nil -} - -func collectMediaAssetIDs(values ...string) []uuid.UUID { - seen := map[uuid.UUID]struct{}{} - assetIDs := make([]uuid.UUID, 0) - for _, value := range values { - for _, match := range mediaObjectRefPattern.FindAllStringSubmatch(value, -1) { - if len(match) != 2 || !strings.HasPrefix(match[0], mediaObjectRefPrefix) { - continue - } - assetID, err := uuid.Parse(match[1]) - if err != nil { - continue - } - if _, ok := seen[assetID]; ok { - continue - } - seen[assetID] = struct{}{} - assetIDs = append(assetIDs, assetID) - } - } - return assetIDs -} diff --git a/backend/internal/services/project/mediausage/media_usage.go b/backend/internal/services/project/mediausage/media_usage.go new file mode 100644 index 00000000..a77da27e --- /dev/null +++ b/backend/internal/services/project/mediausage/media_usage.go @@ -0,0 +1,106 @@ +package mediausage + +import ( + "regexp" + "strings" + + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "github.com/kurodakayn/mpp-backend/internal/models" + "github.com/kurodakayn/mpp-backend/internal/services/project/projecterr" +) + +const objectRefPrefix = "mpp://media/" + +var objectRefPattern = regexp.MustCompile(`mpp://media/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})`) + +func RefreshProject(tx *gorm.DB, project models.Project, publications []models.ProjectPlatformPublication) error { + workspaceID := projectWorkspaceID(project) + if err := tx.Where("project_id = ?", project.ID).Delete(&models.MediaAssetUsage{}).Error; err != nil { + return err + } + + sourceRefs := collectAssetIDs(project.SourceContent) + if err := upsert(tx, workspaceID, &project.ID, nil, nil, "project", project.ID, models.MediaAssetUsageEditorImage, sourceRefs); err != nil { + return err + } + for _, publication := range publications { + refs := collectAssetIDs(string(publication.Config), string(publication.AdaptedContent)) + if len(refs) == 0 { + continue + } + if err := upsert(tx, workspaceID, &project.ID, &publication.ID, nil, "publication", publication.ID, publication.Platform, refs); err != nil { + return err + } + } + return nil +} + +func RefreshContentTemplate(tx *gorm.DB, workspaceID uuid.UUID, template models.ContentTemplate) error { + if template.ID == uuid.Nil { + return projecterr.ErrInvalidProject + } + if err := tx.Where("template_id = ?", template.ID).Delete(&models.MediaAssetUsage{}).Error; err != nil { + return err + } + + refs := collectAssetIDs(template.SourceTemplate, string(template.PlatformConfig)) + return upsert(tx, workspaceID, nil, nil, &template.ID, "template", template.ID, models.MediaAssetUsageEditorImage, refs) +} + +func upsert(tx *gorm.DB, workspaceID uuid.UUID, projectID *uuid.UUID, publicationID *uuid.UUID, templateID *uuid.UUID, resourceType string, resourceID uuid.UUID, usageKind string, assetIDs []uuid.UUID) error { + for _, assetID := range assetIDs { + usage := models.MediaAssetUsage{ + MediaAssetID: assetID, + WorkspaceID: workspaceID, + ProjectID: projectID, + PublicationID: publicationID, + TemplateID: templateID, + ResourceType: resourceType, + ResourceID: resourceID, + UsageKind: usageKind, + } + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "media_asset_id"}, + {Name: "resource_type"}, + {Name: "resource_id"}, + }, + DoNothing: true, + }).Create(&usage).Error; err != nil { + return err + } + } + return nil +} + +func collectAssetIDs(values ...string) []uuid.UUID { + seen := map[uuid.UUID]struct{}{} + assetIDs := make([]uuid.UUID, 0) + for _, value := range values { + for _, match := range objectRefPattern.FindAllStringSubmatch(value, -1) { + if len(match) != 2 || !strings.HasPrefix(match[0], objectRefPrefix) { + continue + } + assetID, err := uuid.Parse(match[1]) + if err != nil { + continue + } + if _, ok := seen[assetID]; ok { + continue + } + seen[assetID] = struct{}{} + assetIDs = append(assetIDs, assetID) + } + } + return assetIDs +} + +func projectWorkspaceID(project models.Project) uuid.UUID { + if project.WorkspaceID != nil && *project.WorkspaceID != uuid.Nil { + return *project.WorkspaceID + } + return models.PersonalWorkspaceID(project.UserID) +} diff --git a/backend/internal/services/project/media_usage_test.go b/backend/internal/services/project/mediausage/media_usage_test.go similarity index 79% rename from backend/internal/services/project/media_usage_test.go rename to backend/internal/services/project/mediausage/media_usage_test.go index 41898898..aecbb0b3 100644 --- a/backend/internal/services/project/media_usage_test.go +++ b/backend/internal/services/project/mediausage/media_usage_test.go @@ -1,4 +1,4 @@ -package project +package mediausage import ( "testing" @@ -10,7 +10,7 @@ import ( "github.com/kurodakayn/mpp-backend/internal/services/testsupport" ) -func TestUpsertMediaUsagesIgnoresDuplicateAssetResourceRefs(t *testing.T) { +func TestUpsertIgnoresDuplicateAssetResourceRefs(t *testing.T) { db := testsupport.SetupTestDB() require.NoError(t, db.AutoMigrate(&models.MediaAsset{})) @@ -40,8 +40,8 @@ func TestUpsertMediaUsagesIgnoresDuplicateAssetResourceRefs(t *testing.T) { require.NoError(t, db.Create(&asset).Error) resourceID := uuid.New() - require.NoError(t, upsertMediaUsages(db, workspace.ID, nil, nil, nil, "template", resourceID, models.MediaAssetUsageEditorImage, []uuid.UUID{asset.ID})) - require.NoError(t, upsertMediaUsages(db, workspace.ID, nil, nil, nil, "template", resourceID, models.MediaAssetUsageCoverImage, []uuid.UUID{asset.ID})) + require.NoError(t, upsert(db, workspace.ID, nil, nil, nil, "template", resourceID, models.MediaAssetUsageEditorImage, []uuid.UUID{asset.ID})) + require.NoError(t, upsert(db, workspace.ID, nil, nil, nil, "template", resourceID, models.MediaAssetUsageCoverImage, []uuid.UUID{asset.ID})) var count int64 require.NoError(t, db.Model(&models.MediaAssetUsage{}). From 2b39085b9493c0401b53738641bec65aa76173fe Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 21 Jun 2026 09:47:11 +0800 Subject: [PATCH 3/6] refactor(project-publication): extract publication helpers Publication platform validation, default config building, and detail shaping were scattered through the root project package. Move those helpers into a publication package and keep thin wrappers for existing project service callers. Add focused tests for the detail response variants so existing response shapes stay stable. --- backend/internal/services/project/detail.go | 40 +----- .../internal/services/project/platforms.go | 25 ---- .../project/publication/publication.go | 124 ++++++++++++++++++ .../project/publication/publication_test.go | 33 +++++ .../services/project/publication_config.go | 25 ---- .../internal/services/project/publications.go | 75 +++-------- backend/internal/services/project/service.go | 3 - 7 files changed, 174 insertions(+), 151 deletions(-) delete mode 100644 backend/internal/services/project/platforms.go create mode 100644 backend/internal/services/project/publication/publication.go create mode 100644 backend/internal/services/project/publication/publication_test.go delete mode 100644 backend/internal/services/project/publication_config.go diff --git a/backend/internal/services/project/detail.go b/backend/internal/services/project/detail.go index c0af1531..0c59cb52 100644 --- a/backend/internal/services/project/detail.go +++ b/backend/internal/services/project/detail.go @@ -1,7 +1,6 @@ package project import ( - "encoding/json" "errors" "github.com/google/uuid" @@ -9,6 +8,7 @@ import ( "github.com/kurodakayn/mpp-backend/internal/dto" "github.com/kurodakayn/mpp-backend/internal/models" + projectpublication "github.com/kurodakayn/mpp-backend/internal/services/project/publication" ) func (s *Service) enrichProjectDetail(detail *dto.ProjectDetail, project models.Project, userID *uuid.UUID) error { @@ -17,7 +17,7 @@ func (s *Service) enrichProjectDetail(detail *dto.ProjectDetail, project models. } detail.PublicationDetails = make([]dto.PublicationDetail, 0, len(project.Publications)) for _, publication := range project.Publications { - detail.PublicationDetails = append(detail.PublicationDetails, publicationDetailFromModel(publication, true)) + detail.PublicationDetails = append(detail.PublicationDetails, projectpublication.DetailFromModel(publication, true)) } if userID == nil { if detail.PermissionSources == nil { @@ -110,39 +110,3 @@ func appendProjectPermissionSource(sources []dto.ProjectPermissionSource, source } return append(sources, source) } - -func publicationDetailFromModel(pub models.ProjectPlatformPublication, includeContent bool) dto.PublicationDetail { - var rawConfig map[string]any - _ = json.Unmarshal(pub.Config, &rawConfig) - safeConfig := filterConfig(rawConfig) - - var rawContent map[string]any - _ = json.Unmarshal(pub.AdaptedContent, &rawContent) - safeContent := rawContent - if !includeContent { - safeContent = summarizeAdaptedContent(rawContent) - } - if safeContent == nil { - safeContent = map[string]any{} - } - - return dto.PublicationDetail{ - ID: pub.ID, - Platform: pub.Platform, - Enabled: pub.Enabled, - Status: pub.Status, - DraftStatus: pub.DraftStatus, - ReviewStatus: pub.ReviewStatus, - SyncRequired: pub.SyncRequired, - ErrorMessage: pub.ErrorMessage, - Config: safeConfig, - AdaptedContent: safeContent, - PublishURL: pub.PublishURL, - RemoteID: pub.RemoteID, - RetryCount: pub.RetryCount, - LastAttemptAt: pub.LastAttemptAt, - PublishedAt: pub.PublishedAt, - CreatedAt: pub.CreatedAt, - UpdatedAt: pub.UpdatedAt, - } -} diff --git a/backend/internal/services/project/platforms.go b/backend/internal/services/project/platforms.go deleted file mode 100644 index b73dd8e2..00000000 --- a/backend/internal/services/project/platforms.go +++ /dev/null @@ -1,25 +0,0 @@ -package project - -import "strings" - -func NormalizeProjectPlatforms(input []string) ([]string, error) { - seen := map[string]struct{}{} - platforms := make([]string, 0, len(input)) - - for _, raw := range input { - platform := strings.TrimSpace(raw) - if platform == "" { - continue - } - if _, ok := allowedProjectPlatforms[platform]; !ok { - return nil, ErrInvalidProject - } - if _, ok := seen[platform]; ok { - continue - } - seen[platform] = struct{}{} - platforms = append(platforms, platform) - } - - return platforms, nil -} diff --git a/backend/internal/services/project/publication/publication.go b/backend/internal/services/project/publication/publication.go new file mode 100644 index 00000000..2dc87dfe --- /dev/null +++ b/backend/internal/services/project/publication/publication.go @@ -0,0 +1,124 @@ +package publication + +import ( + "encoding/json" + "strings" + + "gorm.io/datatypes" + + "github.com/kurodakayn/mpp-backend/internal/dto" + "github.com/kurodakayn/mpp-backend/internal/models" + platformcapabilities "github.com/kurodakayn/mpp-backend/internal/platformcapabilities" + "github.com/kurodakayn/mpp-backend/internal/services/project/contentsetup" + "github.com/kurodakayn/mpp-backend/internal/services/project/projecterr" + "github.com/kurodakayn/mpp-backend/internal/services/project/publicationselection" + "github.com/kurodakayn/mpp-backend/internal/services/publicationpayload" +) + +var allowedPlatforms = platformcapabilities.ProjectPlatformSet() + +func NormalizePlatforms(input []string) ([]string, error) { + seen := map[string]struct{}{} + platforms := make([]string, 0, len(input)) + + for _, raw := range input { + platform := strings.TrimSpace(raw) + if platform == "" { + continue + } + if _, ok := allowedPlatforms[platform]; !ok { + return nil, projecterr.ErrInvalidProject + } + if _, ok := seen[platform]; ok { + continue + } + seen[platform] = struct{}{} + platforms = append(platforms, platform) + } + + return platforms, nil +} + +func PendingConfigForTemplate(title, summary, coverImageURL string, template *models.ContentTemplate) publicationselection.ConfigForPlatform { + return func(platform string) (datatypes.JSON, error) { + config, err := publicationpayload.DefaultConfig(title, summary, coverImageURL) + if err != nil { + return nil, err + } + return contentsetup.MergePublicationConfig(config, contentsetup.ContentTemplatePlatformConfig(template, platform)) + } +} + +func DefaultConfigForProjectTitle(title string) publicationselection.ConfigForPlatform { + return func(string) (datatypes.JSON, error) { + return publicationpayload.DefaultConfig(title, "", "") + } +} + +func DetailFromModel(pub models.ProjectPlatformPublication, includeContent bool) dto.PublicationDetail { + return detailFromModel(pub, includeContent, true) +} + +func ResponseDetailFromModel(pub models.ProjectPlatformPublication, includeContent bool) dto.PublicationDetail { + return detailFromModel(pub, includeContent, false) +} + +func detailFromModel(pub models.ProjectPlatformPublication, includeContent bool, normalizeEmptyContent bool) dto.PublicationDetail { + var rawConfig map[string]any + _ = json.Unmarshal(pub.Config, &rawConfig) + safeConfig := FilterConfig(rawConfig) + + var rawContent map[string]any + _ = json.Unmarshal(pub.AdaptedContent, &rawContent) + safeContent := rawContent + if !includeContent { + safeContent = SummarizeAdaptedContent(rawContent) + } + if normalizeEmptyContent && safeContent == nil { + safeContent = map[string]any{} + } + + return dto.PublicationDetail{ + ID: pub.ID, + Platform: pub.Platform, + Enabled: pub.Enabled, + Status: pub.Status, + DraftStatus: pub.DraftStatus, + ReviewStatus: pub.ReviewStatus, + SyncRequired: pub.SyncRequired, + ErrorMessage: pub.ErrorMessage, + Config: safeConfig, + AdaptedContent: safeContent, + PublishURL: pub.PublishURL, + RemoteID: pub.RemoteID, + RetryCount: pub.RetryCount, + LastAttemptAt: pub.LastAttemptAt, + PublishedAt: pub.PublishedAt, + CreatedAt: pub.CreatedAt, + UpdatedAt: pub.UpdatedAt, + } +} + +func FilterConfig(raw map[string]any) map[string]any { + safe := make(map[string]any) + allowedKeys := []string{"title", "tags", "cover_image", "topics", "category", "original_declaration", "username"} + for _, key := range allowedKeys { + if val, ok := raw[key]; ok { + safe[key] = val + } + } + return safe +} + +func SummarizeAdaptedContent(raw map[string]any) map[string]any { + safe := make(map[string]any) + if summary, ok := raw["summary"]; ok { + safe["summary"] = summary + } else { + safe["summary"] = "Content adapted (no summary available)" + } + if format, ok := raw["format"]; ok { + safe["format"] = format + } + return safe +} diff --git a/backend/internal/services/project/publication/publication_test.go b/backend/internal/services/project/publication/publication_test.go new file mode 100644 index 00000000..e85a7ce6 --- /dev/null +++ b/backend/internal/services/project/publication/publication_test.go @@ -0,0 +1,33 @@ +package publication + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gorm.io/datatypes" + + "github.com/kurodakayn/mpp-backend/internal/models" +) + +func TestDetailFromModelNormalizesEmptyContent(t *testing.T) { + detail := DetailFromModel(models.ProjectPlatformPublication{}, true) + + require.NotNil(t, detail.AdaptedContent) + require.Empty(t, detail.AdaptedContent) +} + +func TestResponseDetailFromModelPreservesEmptyIncludedContent(t *testing.T) { + detail := ResponseDetailFromModel(models.ProjectPlatformPublication{}, true) + + require.Nil(t, detail.AdaptedContent) +} + +func TestResponseDetailFromModelSummarizesExcludedContent(t *testing.T) { + pub := models.ProjectPlatformPublication{ + AdaptedContent: datatypes.JSON([]byte(`{"summary":"short","body":"hidden"}`)), + } + + detail := ResponseDetailFromModel(pub, false) + + require.Equal(t, map[string]any{"summary": "short"}, detail.AdaptedContent) +} diff --git a/backend/internal/services/project/publication_config.go b/backend/internal/services/project/publication_config.go deleted file mode 100644 index a51ef53d..00000000 --- a/backend/internal/services/project/publication_config.go +++ /dev/null @@ -1,25 +0,0 @@ -package project - -import ( - "gorm.io/datatypes" - - "github.com/kurodakayn/mpp-backend/internal/models" - "github.com/kurodakayn/mpp-backend/internal/services/project/publicationselection" - "github.com/kurodakayn/mpp-backend/internal/services/publicationpayload" -) - -func pendingPublicationConfigForTemplate(title, summary, coverImageURL string, template *models.ContentTemplate) publicationselection.ConfigForPlatform { - return func(platform string) (datatypes.JSON, error) { - config, err := publicationpayload.DefaultConfig(title, summary, coverImageURL) - if err != nil { - return nil, err - } - return mergePublicationConfig(config, contentTemplatePlatformConfig(template, platform)) - } -} - -func defaultPublicationConfigForProjectTitle(title string) publicationselection.ConfigForPlatform { - return func(string) (datatypes.JSON, error) { - return publicationpayload.DefaultConfig(title, "", "") - } -} diff --git a/backend/internal/services/project/publications.go b/backend/internal/services/project/publications.go index ea6878e8..ec61444f 100644 --- a/backend/internal/services/project/publications.go +++ b/backend/internal/services/project/publications.go @@ -1,14 +1,26 @@ package project import ( - "encoding/json" - "github.com/google/uuid" "github.com/kurodakayn/mpp-backend/internal/dto" "github.com/kurodakayn/mpp-backend/internal/models" + projectpublication "github.com/kurodakayn/mpp-backend/internal/services/project/publication" + "github.com/kurodakayn/mpp-backend/internal/services/project/publicationselection" ) +func NormalizeProjectPlatforms(input []string) ([]string, error) { + return projectpublication.NormalizePlatforms(input) +} + +func pendingPublicationConfigForTemplate(title, summary, coverImageURL string, template *models.ContentTemplate) publicationselection.ConfigForPlatform { + return projectpublication.PendingConfigForTemplate(title, summary, coverImageURL, template) +} + +func defaultPublicationConfigForProjectTitle(title string) publicationselection.ConfigForPlatform { + return projectpublication.DefaultConfigForProjectTitle(title) +} + func (s *Service) GetProjectPublications(projectID uuid.UUID, scopeUserID *uuid.UUID, includeContent bool) (*dto.ProjectPublicationsResponse, error) { readDB := s.projectDetailReadDB(scopeUserID) @@ -31,38 +43,7 @@ func (s *Service) GetProjectPublications(projectID uuid.UUID, scopeUserID *uuid. var items []dto.PublicationDetail for _, pub := range publications { - // Safe parse config - var rawConfig map[string]any - _ = json.Unmarshal(pub.Config, &rawConfig) - safeConfig := filterConfig(rawConfig) - - // Safe parse adapted content - var rawContent map[string]any - _ = json.Unmarshal(pub.AdaptedContent, &rawContent) - safeContent := rawContent - if !includeContent { - safeContent = summarizeAdaptedContent(rawContent) - } - - items = append(items, dto.PublicationDetail{ - ID: pub.ID, - Platform: pub.Platform, - Enabled: pub.Enabled, - Status: pub.Status, - DraftStatus: pub.DraftStatus, - ReviewStatus: pub.ReviewStatus, - SyncRequired: pub.SyncRequired, - ErrorMessage: pub.ErrorMessage, - Config: safeConfig, - AdaptedContent: safeContent, - PublishURL: pub.PublishURL, - RemoteID: pub.RemoteID, - RetryCount: pub.RetryCount, - LastAttemptAt: pub.LastAttemptAt, - PublishedAt: pub.PublishedAt, - CreatedAt: pub.CreatedAt, - UpdatedAt: pub.UpdatedAt, - }) + items = append(items, projectpublication.ResponseDetailFromModel(pub, includeContent)) } if items == nil { @@ -74,29 +55,3 @@ func (s *Service) GetProjectPublications(projectID uuid.UUID, scopeUserID *uuid. Items: items, }, nil } - -// Helper functions to filter sensitive data from JSONB fields - -func filterConfig(raw map[string]any) map[string]any { - safe := make(map[string]any) - allowedKeys := []string{"title", "tags", "cover_image", "topics", "category", "original_declaration", "username"} - for _, key := range allowedKeys { - if val, ok := raw[key]; ok { - safe[key] = val - } - } - return safe -} - -func summarizeAdaptedContent(raw map[string]any) map[string]any { - safe := make(map[string]any) - if summary, ok := raw["summary"]; ok { - safe["summary"] = summary - } else { - safe["summary"] = "Content adapted (no summary available)" - } - if format, ok := raw["format"]; ok { - safe["format"] = format - } - return safe -} diff --git a/backend/internal/services/project/service.go b/backend/internal/services/project/service.go index dac92891..04728619 100644 --- a/backend/internal/services/project/service.go +++ b/backend/internal/services/project/service.go @@ -14,7 +14,6 @@ import ( dbrouter "github.com/kurodakayn/mpp-backend/internal/db" "github.com/kurodakayn/mpp-backend/internal/models" "github.com/kurodakayn/mpp-backend/internal/pkg/redisdegrade" - platformcapabilities "github.com/kurodakayn/mpp-backend/internal/platformcapabilities" "github.com/kurodakayn/mpp-backend/internal/services/accesspolicy" collabdoc "github.com/kurodakayn/mpp-backend/internal/services/collabdoc" "github.com/kurodakayn/mpp-backend/internal/services/project/projecterr" @@ -38,8 +37,6 @@ type DashboardReadModelUpdater interface { RefreshWorkspaceAsync(ctx context.Context, workspaceID uuid.UUID) } -var allowedProjectPlatforms = platformcapabilities.ProjectPlatformSet() - type Service struct { db *gorm.DB router *dbrouter.Router From 7f355426fba7f9b522871a70d224750c286034fa Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 21 Jun 2026 09:47:44 +0800 Subject: [PATCH 4/6] refactor(project-listing): fold list cache into listing Dashboard project list cache code belongs with the rest of the project listing mechanics. Move the cache implementation and key tests from the cache package into listing and update the root facade import. This removes the extra cache directory while keeping the list cache behavior unchanged. --- backend/internal/services/project/list_cache.go | 12 ++++++------ .../services/project/{cache => listing}/cache.go | 2 +- .../cache_test.go} | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename backend/internal/services/project/{cache => listing}/cache.go (99%) rename backend/internal/services/project/{cache/list_cache_keys_test.go => listing/cache_test.go} (97%) diff --git a/backend/internal/services/project/list_cache.go b/backend/internal/services/project/list_cache.go index 3eff82bb..55ffabcd 100644 --- a/backend/internal/services/project/list_cache.go +++ b/backend/internal/services/project/list_cache.go @@ -6,14 +6,14 @@ import ( "github.com/google/uuid" "github.com/kurodakayn/mpp-backend/internal/dto" - projectcache "github.com/kurodakayn/mpp-backend/internal/services/project/cache" + projectlisting "github.com/kurodakayn/mpp-backend/internal/services/project/listing" ) -func (s *Service) projectListCache() *projectcache.Cache { +func (s *Service) projectListCache() *projectlisting.Cache { if s == nil { return nil } - return projectcache.New(projectcache.Config{ + return projectlisting.New(projectlisting.Config{ Client: s.cache, TTL: s.cacheTTL, Group: s.cacheGroup, @@ -22,14 +22,14 @@ func (s *Service) projectListCache() *projectcache.Cache { } func (s *Service) getCachedDashboardProjectList(cursor string, page, limit int, status, filterUserID, platform string, scopeUserID *uuid.UUID) (*dto.PaginationResponse, error) { - params := projectcache.Params{ + params := projectlisting.Params{ Cursor: cursor, Page: page, Limit: limit, Status: status, FilterUserID: filterUserID, Platform: platform, - ScopeUserID: projectcache.UUIDStringValue(scopeUserID), + ScopeUserID: projectlisting.UUIDStringValue(scopeUserID), } return s.projectListCache().Get(s.requestContext(), params, func(ctx context.Context) (*dto.PaginationResponse, error) { return s.WithContext(ctx).computeProjectList(cursor, page, limit, status, filterUserID, platform, scopeUserID) @@ -37,7 +37,7 @@ func (s *Service) getCachedDashboardProjectList(cursor string, page, limit int, } func (s *Service) getCachedWorkspaceProjectList(workspaceID uuid.UUID, actorUserID uuid.UUID, cursor string, page, limit int, status, platform string) (*dto.PaginationResponse, error) { - params := projectcache.Params{ + params := projectlisting.Params{ Cursor: cursor, Page: page, Limit: limit, diff --git a/backend/internal/services/project/cache/cache.go b/backend/internal/services/project/listing/cache.go similarity index 99% rename from backend/internal/services/project/cache/cache.go rename to backend/internal/services/project/listing/cache.go index f456a886..be70e0b9 100644 --- a/backend/internal/services/project/cache/cache.go +++ b/backend/internal/services/project/listing/cache.go @@ -1,4 +1,4 @@ -package cache +package listing import ( "context" diff --git a/backend/internal/services/project/cache/list_cache_keys_test.go b/backend/internal/services/project/listing/cache_test.go similarity index 97% rename from backend/internal/services/project/cache/list_cache_keys_test.go rename to backend/internal/services/project/listing/cache_test.go index a7314c40..17e2c342 100644 --- a/backend/internal/services/project/cache/list_cache_keys_test.go +++ b/backend/internal/services/project/listing/cache_test.go @@ -1,4 +1,4 @@ -package cache +package listing import ( "testing" From 4e4e2f912a3e98e1214f426c251d4fec9cd3aee0 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 21 Jun 2026 09:51:46 +0800 Subject: [PATCH 5/6] refactor(project-listing): fold cache facade into list service The root project directory still had separate list cache files after moving the cache implementation into listing. Merge the project service cache facade into list.go and move the integration cache tests under listing. Project listing code keeps the required Service methods without leaving standalone list cache files in the root package. --- backend/internal/services/project/list.go | 90 +++++++++++++++++ .../internal/services/project/list_cache.go | 98 ------------------- .../list_cache_integration_test.go} | 2 +- 3 files changed, 91 insertions(+), 99 deletions(-) delete mode 100644 backend/internal/services/project/list_cache.go rename backend/internal/services/project/{list_cache_test.go => listing/list_cache_integration_test.go} (99%) diff --git a/backend/internal/services/project/list.go b/backend/internal/services/project/list.go index 657f5f83..e8d93f4a 100644 --- a/backend/internal/services/project/list.go +++ b/backend/internal/services/project/list.go @@ -1,6 +1,8 @@ package project import ( + "context" + "github.com/google/uuid" "gorm.io/gorm" @@ -29,6 +31,94 @@ func (s *Service) ListCachedWorkspaceProjects(workspaceID uuid.UUID, actorUserID return s.getCachedWorkspaceProjectList(workspaceID, actorUserID, cursor, page, limit, status, platform) } +func (s *Service) projectListCache() *projectlisting.Cache { + if s == nil { + return nil + } + return projectlisting.New(projectlisting.Config{ + Client: s.cache, + TTL: s.cacheTTL, + Group: s.cacheGroup, + Guard: s.projectListGuard, + }) +} + +func (s *Service) getCachedDashboardProjectList(cursor string, page, limit int, status, filterUserID, platform string, scopeUserID *uuid.UUID) (*dto.PaginationResponse, error) { + params := projectlisting.Params{ + Cursor: cursor, + Page: page, + Limit: limit, + Status: status, + FilterUserID: filterUserID, + Platform: platform, + ScopeUserID: projectlisting.UUIDStringValue(scopeUserID), + } + return s.projectListCache().Get(s.requestContext(), params, func(ctx context.Context) (*dto.PaginationResponse, error) { + return s.WithContext(ctx).computeProjectList(cursor, page, limit, status, filterUserID, platform, scopeUserID) + }) +} + +func (s *Service) getCachedWorkspaceProjectList(workspaceID uuid.UUID, actorUserID uuid.UUID, cursor string, page, limit int, status, platform string) (*dto.PaginationResponse, error) { + params := projectlisting.Params{ + Cursor: cursor, + Page: page, + Limit: limit, + Status: status, + Platform: platform, + WorkspaceID: workspaceID.String(), + ActorUserID: actorUserID.String(), + } + return s.projectListCache().Get(s.requestContext(), params, func(ctx context.Context) (*dto.PaginationResponse, error) { + return s.WithContext(ctx).computeWorkspaceProjectList(workspaceID, actorUserID, cursor, page, limit, status, platform) + }) +} + +func (s *Service) canUseDashboardProjectListCache() bool { + if s == nil { + return false + } + return s.projectListCache().CanUse(s.requestContext()) +} + +func (s *Service) InvalidateDashboardProjectListCache(ctx context.Context) { + if s == nil { + return + } + if ctx != nil { + s = s.WithContext(ctx) + } + s.invalidateDashboardProjectListCache() +} + +func (s *Service) invalidateDashboardProjectListCache() { + if s == nil { + return + } + s.projectListCache().Invalidate(s.requestContext()) +} + +func (s *Service) invalidateDashboardCaches(includeStats bool) { + ctx := s.requestContext() + s.InvalidateDashboardProjectListCache(ctx) + if includeStats && s.statsCache != nil { + s.statsCache.InvalidateDashboardStatsCache(ctx) + } +} + +func (s *Service) invalidateDashboardScopedStatsCache() { + if s.statsCache == nil { + return + } + s.statsCache.InvalidateDashboardScopedStatsCache(s.requestContext()) +} + +func (s *Service) refreshProjectReadModel(projectID uuid.UUID) { + if s.readModels == nil || projectID == uuid.Nil { + return + } + s.readModels.RefreshProjectAsync(s.requestContext(), projectID) +} + func (s *Service) computeProjectList(cursor string, page, limit int, status, filterUserID, platform string, scopeUserID *uuid.UUID) (*dto.PaginationResponse, error) { if scopeUserID == nil && platform == "" && s.canUseReadModels() { if resp, ok, err := s.adminProjectListFromReadModel(cursor, page, limit, status, filterUserID); err != nil { diff --git a/backend/internal/services/project/list_cache.go b/backend/internal/services/project/list_cache.go deleted file mode 100644 index 55ffabcd..00000000 --- a/backend/internal/services/project/list_cache.go +++ /dev/null @@ -1,98 +0,0 @@ -package project - -import ( - "context" - - "github.com/google/uuid" - - "github.com/kurodakayn/mpp-backend/internal/dto" - projectlisting "github.com/kurodakayn/mpp-backend/internal/services/project/listing" -) - -func (s *Service) projectListCache() *projectlisting.Cache { - if s == nil { - return nil - } - return projectlisting.New(projectlisting.Config{ - Client: s.cache, - TTL: s.cacheTTL, - Group: s.cacheGroup, - Guard: s.projectListGuard, - }) -} - -func (s *Service) getCachedDashboardProjectList(cursor string, page, limit int, status, filterUserID, platform string, scopeUserID *uuid.UUID) (*dto.PaginationResponse, error) { - params := projectlisting.Params{ - Cursor: cursor, - Page: page, - Limit: limit, - Status: status, - FilterUserID: filterUserID, - Platform: platform, - ScopeUserID: projectlisting.UUIDStringValue(scopeUserID), - } - return s.projectListCache().Get(s.requestContext(), params, func(ctx context.Context) (*dto.PaginationResponse, error) { - return s.WithContext(ctx).computeProjectList(cursor, page, limit, status, filterUserID, platform, scopeUserID) - }) -} - -func (s *Service) getCachedWorkspaceProjectList(workspaceID uuid.UUID, actorUserID uuid.UUID, cursor string, page, limit int, status, platform string) (*dto.PaginationResponse, error) { - params := projectlisting.Params{ - Cursor: cursor, - Page: page, - Limit: limit, - Status: status, - Platform: platform, - WorkspaceID: workspaceID.String(), - ActorUserID: actorUserID.String(), - } - return s.projectListCache().Get(s.requestContext(), params, func(ctx context.Context) (*dto.PaginationResponse, error) { - return s.WithContext(ctx).computeWorkspaceProjectList(workspaceID, actorUserID, cursor, page, limit, status, platform) - }) -} - -func (s *Service) canUseDashboardProjectListCache() bool { - if s == nil { - return false - } - return s.projectListCache().CanUse(s.requestContext()) -} - -func (s *Service) InvalidateDashboardProjectListCache(ctx context.Context) { - if s == nil { - return - } - if ctx != nil { - s = s.WithContext(ctx) - } - s.invalidateDashboardProjectListCache() -} - -func (s *Service) invalidateDashboardProjectListCache() { - if s == nil { - return - } - s.projectListCache().Invalidate(s.requestContext()) -} - -func (s *Service) invalidateDashboardCaches(includeStats bool) { - ctx := s.requestContext() - s.InvalidateDashboardProjectListCache(ctx) - if includeStats && s.statsCache != nil { - s.statsCache.InvalidateDashboardStatsCache(ctx) - } -} - -func (s *Service) invalidateDashboardScopedStatsCache() { - if s.statsCache == nil { - return - } - s.statsCache.InvalidateDashboardScopedStatsCache(s.requestContext()) -} - -func (s *Service) refreshProjectReadModel(projectID uuid.UUID) { - if s.readModels == nil || projectID == uuid.Nil { - return - } - s.readModels.RefreshProjectAsync(s.requestContext(), projectID) -} diff --git a/backend/internal/services/project/list_cache_test.go b/backend/internal/services/project/listing/list_cache_integration_test.go similarity index 99% rename from backend/internal/services/project/list_cache_test.go rename to backend/internal/services/project/listing/list_cache_integration_test.go index 73ec4959..70cf65b5 100644 --- a/backend/internal/services/project/list_cache_test.go +++ b/backend/internal/services/project/listing/list_cache_integration_test.go @@ -1,4 +1,4 @@ -package project_test +package listing_test import ( "context" From 2772a644bcdae569c2ebfee55fd87b97b27a2cf7 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 21 Jun 2026 09:52:09 +0800 Subject: [PATCH 6/6] refactor(project-publication): remove stale content setup wrappers Publication config helpers now call contentsetup directly from the publication package. Remove the old project package wrapper functions and the no longer needed datatypes import. This keeps the refactor lint-clean without changing content setup behavior. --- backend/internal/services/project/content_setup.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/internal/services/project/content_setup.go b/backend/internal/services/project/content_setup.go index 434ea6cb..401aba85 100644 --- a/backend/internal/services/project/content_setup.go +++ b/backend/internal/services/project/content_setup.go @@ -2,7 +2,6 @@ package project import ( "github.com/google/uuid" - "gorm.io/datatypes" "github.com/kurodakayn/mpp-backend/internal/dto" "github.com/kurodakayn/mpp-backend/internal/models" @@ -50,11 +49,3 @@ func (s *Service) validateBrandProfileForProject(userID uuid.UUID, workspaceID u func contentTemplateDefaultPlatforms(template *models.ContentTemplate) ([]string, error) { return contentsetup.ContentTemplateDefaultPlatforms(template, NormalizeProjectPlatforms) } - -func contentTemplatePlatformConfig(template *models.ContentTemplate, platform string) map[string]any { - return contentsetup.ContentTemplatePlatformConfig(template, platform) -} - -func mergePublicationConfig(base datatypes.JSON, extra map[string]any) (datatypes.JSON, error) { - return contentsetup.MergePublicationConfig(base, extra) -}