package memory import ( "context" "sync" "img_infinite_canvas/internal/domain/design" ) type ProjectRepository struct { mu sync.RWMutex projects map[string]design.Project mockupBlobs map[string]design.MockupBlobData } func NewProjectRepository() *ProjectRepository { return &ProjectRepository{ projects: make(map[string]design.Project), mockupBlobs: make(map[string]design.MockupBlobData), } } func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary, error) { if err := ctx.Err(); err != nil { return nil, err } r.mu.RLock() defer r.mu.RUnlock() items := make([]design.ProjectSummary, 0, len(r.projects)) userID := design.UserIDFromContext(ctx) for _, project := range r.projects { if project.UserID == "" { project.UserID = design.DefaultUserID } if project.UserID != userID { continue } items = append(items, design.ProjectSummary{ ID: project.ID, UserID: project.UserID, Title: project.Title, Brief: project.Brief, Status: project.Status, Thumbnail: project.Thumbnail, UpdatedAt: project.UpdatedAt, }) } return items, nil } func (r *ProjectRepository) ListPage(ctx context.Context, limit int64, offset int64) ([]design.ProjectSummary, error) { items, err := r.List(ctx) if err != nil { return nil, err } design.SortSummaries(items) if offset < 0 { offset = 0 } if limit <= 0 { limit = int64(len(items)) } start := int(offset) if start >= len(items) { return []design.ProjectSummary{}, nil } end := start + int(limit) if end > len(items) { end = len(items) } return items[start:end], nil } func (r *ProjectRepository) Get(ctx context.Context, id string) (design.Project, error) { if err := ctx.Err(); err != nil { return design.Project{}, err } r.mu.RLock() defer r.mu.RUnlock() project, ok := r.projects[id] if !ok { return design.Project{}, design.ErrNotFound } if project.UserID == "" { project.UserID = design.DefaultUserID } if project.UserID != design.UserIDFromContext(ctx) { return design.Project{}, design.ErrNotFound } return cloneProject(project), nil } func (r *ProjectRepository) Save(ctx context.Context, project design.Project) error { if err := ctx.Err(); err != nil { return err } r.mu.Lock() defer r.mu.Unlock() project.UserID = repositoryUserID(ctx, project.UserID) r.projects[project.ID] = cloneProject(project) return nil } func (r *ProjectRepository) SaveCanvas(ctx context.Context, project design.Project, baseVersion string) error { if err := ctx.Err(); err != nil { return err } r.mu.Lock() defer r.mu.Unlock() userID := repositoryUserID(ctx, project.UserID) current, ok := r.projects[project.ID] if !ok { return design.ErrNotFound } if current.UserID == "" { current.UserID = design.DefaultUserID } if current.UserID != userID { return design.ErrNotFound } if baseVersion != "" && current.Version != "" && baseVersion != current.Version { return design.ErrConflict } current.UserID = userID current.Title = project.Title current.Brief = project.Brief current.Status = project.Status current.Thumbnail = project.Thumbnail current.UpdatedAt = project.UpdatedAt current.Version = project.Version current.SnapshotID = project.SnapshotID current.Canvas = project.Canvas current.LastThreadID = project.LastThreadID current.Viewport = project.Viewport current.Nodes = append([]design.Node(nil), project.Nodes...) current.Connections = append([]design.Connection(nil), project.Connections...) r.projects[project.ID] = cloneProject(current) return nil } func (r *ProjectRepository) Delete(ctx context.Context, id string) error { if err := ctx.Err(); err != nil { return err } r.mu.Lock() defer r.mu.Unlock() if _, ok := r.projects[id]; !ok { return design.ErrNotFound } project := r.projects[id] if project.UserID == "" { project.UserID = design.DefaultUserID } if project.UserID != design.UserIDFromContext(ctx) { return design.ErrNotFound } delete(r.projects, id) return nil } func (r *ProjectRepository) SaveMockupBlob(ctx context.Context, blob design.MockupBlobData) error { if err := ctx.Err(); err != nil { return err } if blob.DataID == "" { return design.ErrInvalidInput } r.mu.Lock() defer r.mu.Unlock() r.mockupBlobs[blob.DataID] = cloneMockupBlob(blob) return nil } func (r *ProjectRepository) GetMockupBlob(ctx context.Context, dataID string) (design.MockupBlobData, error) { if err := ctx.Err(); err != nil { return design.MockupBlobData{}, err } r.mu.RLock() defer r.mu.RUnlock() blob, ok := r.mockupBlobs[dataID] if !ok { return design.MockupBlobData{}, design.ErrNotFound } return cloneMockupBlob(blob), nil } func repositoryUserID(ctx context.Context, userID string) string { if userID != "" { return design.NormalizeUserID(userID) } return design.UserIDFromContext(ctx) } func cloneProject(project design.Project) design.Project { project.UserID = repositoryUserID(context.Background(), project.UserID) project.Nodes = append([]design.Node(nil), project.Nodes...) project.Connections = append([]design.Connection(nil), project.Connections...) project.Messages = append([]design.Message(nil), project.Messages...) return project } func cloneMockupBlob(blob design.MockupBlobData) design.MockupBlobData { blob.Data = append([]byte(nil), blob.Data...) return blob }