Repository: reearth/reearth-visualizer
Pull request #1573: feat(server): Add Public Projects Support[VIZ-1481] [VIZ-1480]
Author: login: hexaforce
Labels: name: server
Created: 2025-04-17T06:38:09Z | Merged: 2025-04-17T08:29:54Z

# Overview
##  added a visibility parameter to projects.

## What I've done

### 1. visibility (string) will be added to the project.
```diff
type Project implements Node {
  ...
  trackingId: String!
  starred: Boolean!
  isDeleted: Boolean!
+  visibility: String!
}
```

### 2. The visibility can be set to either "public" or "private" (as strings).
If not specified during createProject, it will default to "private".
```diff
input CreateProjectInput {
  teamId: ID!
  visualizer: Visualizer!
  name: String
  description: String
  coreSupport: Boolean
+  visibility: String
}
```
### 3. The visibility can be updated using updateProject.
```diff
input UpdateProjectInput {
  ....
  sceneId: ID
  starred: Boolean
  deleted: Boolean
+  visibility: String
}
```

### 4. In the project's e2e tests, queries and mutations that were defined individually have been refactored and consolidated into a single place.
```
const ProjectFragment = `
fragment ProjectFragment on Project {
    ...

const CreateProjectMutation = `
mutation CreateProject(
    ...

const GetProjectsQuery = `
query GetProjects(
    ...

const UpdateProjectMutation = `
mutation UpdateProject($input: UpdateProjectInput!) {
    ...
```
## What I haven't done

## How I tested

## Which point I want you to review particularly

## Memo

## Please add visibility to the projectFragment in the frontend.
```diff
export const projectFragment = gql`
  fragment ProjectFragment on Project {
    id
    name
    description
    imageUrl
    isArchived
    isBasicAuthActive
    basicAuthUsername
    basicAuthPassword
    publicTitle
    publicDescription
    publicImage
    alias
    enableGa
    trackingId
    publishmentStatus
    updatedAt
    createdAt
    coreSupport
    starred
    isDeleted
+    visibility
  }
`;
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a "visibility" attribute to projects, allowing users to set and update project visibility (e.g., private or public) during creation and editing.
  - Project details now display the visibility status.

- **Tests**
  - Updated and extended test cases to cover project visibility scenarios and ensure correct behavior when setting or changing visibility.
  - Refined project creation in tests to include detailed metadata for better context.

- **Refactor**
  - Improved test structure and consistency for project-related operations by centralizing GraphQL queries and using helper functions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/server/e2e/gql_asset_test.go b/server/e2e/gql_asset_test.go
index a95d68c47a..faa8da8a7d 100644
--- a/server/e2e/gql_asset_test.go
+++ b/server/e2e/gql_asset_test.go
@@ -126,11 +126,23 @@ func TestAssociateProjectGetAssets(t *testing.T) {
 	teamId := wID.String()
 
 	// Create projectA >>> test.png
-	pidA := createProject(e, "projectA")
+	pidA := createProject(e, uID, map[string]any{
+		"name":        "projectA",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	createAsset(t, e, "test.png", true, teamId, &pidA)
 
 	// Create projectB >>> test.csv
-	pidB := createProject(e, "projectB")
+	pidB := createProject(e, uID, map[string]any{
+		"name":        "projectB",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	createAsset(t, e, "test.csv", true, teamId, &pidB)
 
 	// Get projectA >>> test.png
diff --git a/server/e2e/gql_custom_property_test.go b/server/e2e/gql_custom_property_test.go
index 522636e1bd..fb58ef40da 100644
--- a/server/e2e/gql_custom_property_test.go
+++ b/server/e2e/gql_custom_property_test.go
@@ -10,7 +10,13 @@ import (
 
 func TestUpdateCustomProperties(t *testing.T) {
 	e := Server(t, baseSeeder)
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 	lId := addTestNLSLayerSimple(e, sId)
 
@@ -101,7 +107,13 @@ func TestUpdateCustomProperties(t *testing.T) {
 func TestChangeCustomPropertyTitle(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 	lId := addTestNLSLayerSimple(e, sId)
 
@@ -222,7 +234,13 @@ func TestChangeCustomPropertyTitle(t *testing.T) {
 
 func TestRemoveCustomProperty(t *testing.T) {
 	e := Server(t, baseSeeder)
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 	lId := addTestNLSLayerSimple(e, sId)
 
diff --git a/server/e2e/gql_featureCollection_test.go b/server/e2e/gql_featureCollection_test.go
index 01e69ad98e..59d1e79c5e 100644
--- a/server/e2e/gql_featureCollection_test.go
+++ b/server/e2e/gql_featureCollection_test.go
@@ -176,7 +176,13 @@ func deleteGeoJSONFeature(
 func TestFeatureCollectionCRUD(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 
 	_, res := fetchSceneForNewLayers(e, sId)
diff --git a/server/e2e/gql_nlslayer_test.go b/server/e2e/gql_nlslayer_test.go
index 2a46e8c903..b6df967e9c 100644
--- a/server/e2e/gql_nlslayer_test.go
+++ b/server/e2e/gql_nlslayer_test.go
@@ -385,7 +385,13 @@ func fetchSceneForNewLayers(e *httpexpect.Expect, sID string) (GraphQLRequest, *
 func TestNLSLayerCRUD(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 
 	_, notUpdatedProject := fetchProjectForNewLayers(e, pId)
 	notUpdatedProjectUpdatedAt := notUpdatedProject.Object().
@@ -772,7 +778,13 @@ func moveInfoboxBlock(e *httpexpect.Expect, layerId, infoboxBlockId string, inde
 func TestInfoboxBlocksCRUD(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 
 	_, notUpdatedProject := fetchProjectForNewLayers(e, pId)
 	notUpdatedProjectUpdatedAt := notUpdatedProject.Object().
@@ -844,7 +856,13 @@ func TestInfoboxBlocksCRUD(t *testing.T) {
 func TestInfoboxProperty(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 
 	// fetch scene
@@ -896,7 +914,13 @@ func TestInfoboxProperty(t *testing.T) {
 func TestPhotoOverlayProperty(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 
 	// fetch scene
@@ -964,7 +988,13 @@ func updateCustomProperties(
 func TestCustomProperties(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 
 	_, notUpdatedProject := fetchProjectForNewLayers(e, pId)
 	notUpdatedProjectUpdatedAt := notUpdatedProject.Object().
diff --git a/server/e2e/gql_project_test.go b/server/e2e/gql_project_test.go
index fca737e5e3..916bece53b 100644
--- a/server/e2e/gql_project_test.go
+++ b/server/e2e/gql_project_test.go
@@ -16,6 +16,153 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const ProjectFragment = `
+fragment ProjectFragment on Project {
+  id
+  isArchived
+  isBasicAuthActive
+  basicAuthUsername
+  basicAuthPassword
+  createdAt
+  updatedAt
+  publishedAt
+  name
+  description
+  alias
+  publicTitle
+  publicDescription
+  publicImage
+  publicNoIndex
+  imageUrl
+  teamId
+  visualizer
+  publishmentStatus
+  coreSupport
+  enableGa
+  trackingId
+  starred
+  isDeleted
+  visibility
+  __typename
+}`
+
+const GetProjectsQuery = `
+query GetProjects(
+  $teamId: ID!
+  $pagination: Pagination
+  $keyword: String
+  $sort: ProjectSort
+) {
+  projects(
+    teamId: $teamId
+    pagination: $pagination
+    keyword: $keyword
+    sort: $sort
+  ) {
+    edges {
+      node {
+        id
+        ...ProjectFragment
+        scene {
+          id
+          __typename
+        }
+        __typename
+      }
+      __typename
+    }
+    nodes {
+      id
+      ...ProjectFragment
+      scene {
+        id
+        __typename
+      }
+      __typename
+    }
+    pageInfo {
+      endCursor
+      hasNextPage
+      hasPreviousPage
+      startCursor
+      __typename
+    }
+    totalCount
+    __typename
+  }
+}
+` + ProjectFragment
+
+const CreateProjectMutation = `
+mutation CreateProject(
+  $teamId: ID!
+  $visualizer: Visualizer!
+  $name: String
+  $description: String
+  $coreSupport: Boolean
+  $visibility: String
+) {
+  createProject(
+    input: {
+      teamId: $teamId
+      visualizer: $visualizer
+      name: $name
+      description: $description
+      coreSupport: $coreSupport
+	  visibility: $visibility
+    }
+  ) {
+    project {
+	  ...ProjectFragment
+      __typename
+	}
+    __typename
+  }
+}
+` + ProjectFragment
+
+func createProject(e *httpexpect.Expect, u accountdomain.UserID, variables map[string]any) string {
+	requestBody := GraphQLRequest{
+		OperationName: "CreateProject",
+		Query:         CreateProjectMutation,
+		Variables:     variables,
+	}
+	res := Request(e, u.String(), requestBody)
+	return res.Path("$.data.createProject.project.id").Raw().(string)
+}
+
+func createProject2(e *httpexpect.Expect, u accountdomain.UserID, variables map[string]any) *httpexpect.Value {
+	requestBody := GraphQLRequest{
+		OperationName: "CreateProject",
+		Query:         CreateProjectMutation,
+		Variables:     variables,
+	}
+	res := Request(e, u.String(), requestBody)
+	return res.Path("$.data.createProject.project")
+}
+
+const UpdateProjectMutation = `
+mutation UpdateProject($input: UpdateProjectInput!) {
+  updateProject(input: $input) {
+    project {
+	  ...ProjectFragment
+      __typename
+	}
+    __typename
+  }
+}
+` + ProjectFragment
+
+func updateProject(e *httpexpect.Expect, u accountdomain.UserID, variables map[string]any) *httpexpect.Value {
+	requestBody := GraphQLRequest{
+		OperationName: "UpdateProject",
+		Query:         UpdateProjectMutation,
+		Variables:     variables,
+	}
+	return Request(e, u.String(), requestBody).
+		Path("$.data.updateProject.project")
+}
+
 func TestCreateAndGetProject(t *testing.T) {
 	e := Server(t, baseSeeder)
 
@@ -24,7 +171,7 @@ func TestCreateAndGetProject(t *testing.T) {
 	// GetProjects
 	requestBody := GraphQLRequest{
 		OperationName: "GetProjects",
-		Query:         "query GetProjects($teamId: ID!, $pagination: Pagination, $keyword: String, $sort: ProjectSort) {\n projects(\n teamId: $teamId\n pagination: $pagination\n keyword: $keyword\n sort: $sort\n ) {\n edges {\n node {\n id\n ...ProjectFragment\n scene {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n nodes {\n id\n ...ProjectFragment\n scene {\n id\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n __typename\n }\n totalCount\n __typename\n }\n}\n\nfragment ProjectFragment on Project {\n id\n name\n description\n imageUrl\n isArchived\n isBasicAuthActive\n basicAuthUsername\n basicAuthPassword\n publicTitle\n publicDescription\n publicImage\n alias\n enableGa\n trackingId\n publishmentStatus\n updatedAt\n createdAt\n coreSupport\n starred\n isDeleted\n __typename\n}",
+		Query:         GetProjectsQuery,
 		Variables: map[string]any{
 			"teamId": wID.String(),
 			"pagination": map[string]any{
@@ -36,9 +183,8 @@ func TestCreateAndGetProject(t *testing.T) {
 			},
 		},
 	}
-	edges := callRequest(e, requestBody).
-		Value("projects").Object().
-		Value("edges").Array()
+	edges := Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
 
 	edges.Length().IsEqual(2)
 	edges.Value(0).Object().Value("node").Object().Value("name").IsEqual("test2-1")
@@ -56,76 +202,38 @@ func TestCreateAndGetProject(t *testing.T) {
 func TestSortByName(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	createProject(e, "a-project")
-	createProject(e, "b-project")
-	createProject(e, "A-project")
-	createProject(e, "B-project")
+	createProject(e, uID, map[string]any{
+		"name":        "a-project",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "b-project",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "A-project",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "B-project",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 
 	requestBody := GraphQLRequest{
 		OperationName: "GetProjects",
-		Query: `
-		query GetProjects($teamId: ID!, $pagination: Pagination, $keyword: String, $sort: ProjectSort) {
-			projects(
-				teamId: $teamId
-				pagination: $pagination
-				keyword: $keyword
-				sort: $sort
-			) {
-				edges {
-					node {
-						id
-						...ProjectFragment
-						scene {
-							id
-							__typename
-						}
-						__typename
-					}
-					__typename
-				}
-				nodes {
-					id
-					...ProjectFragment
-					scene {
-						id
-						__typename
-					}
-					__typename
-				}
-				pageInfo {
-					endCursor
-					hasNextPage
-					hasPreviousPage
-					startCursor
-					__typename
-				}
-				totalCount
-				__typename
-			}
-		}
-
-		fragment ProjectFragment on Project {
-			id
-			name
-			description
-			imageUrl
-			isArchived
-			isBasicAuthActive
-			basicAuthUsername
-			basicAuthPassword
-			publicTitle
-			publicDescription
-			publicImage
-			alias
-			enableGa
-			trackingId
-			publishmentStatus
-			updatedAt
-			createdAt
-			coreSupport
-			starred
-			__typename
-		}`,
+		Query:         GetProjectsQuery,
 		Variables: map[string]any{
 			"pagination": map[string]any{
 				"last": 5,
@@ -138,9 +246,8 @@ func TestSortByName(t *testing.T) {
 		},
 	}
 
-	edges := callRequest(e, requestBody).
-		Value("projects").Object().
-		Value("edges").Array()
+	edges := Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
 
 	edges.Length().IsEqual(5)
 	edges.Value(0).Object().Value("node").Object().Value("name").IsEqual("a-project")
@@ -153,17 +260,47 @@ func TestSortByName(t *testing.T) {
 func TestFindStarredByWorkspace(t *testing.T) {
 
 	e := Server(t, baseSeeder)
-	project1ID := createProject(e, "Project 1")
-	project2ID := createProject(e, "Project 2")
-	project3ID := createProject(e, "Project 3")
-	project4ID := createProject(e, "Project 4")
-	project5ID := createProject(e, "Project 5")
-
-	starProject(e, project1ID)
-	starProject(e, project3ID)
+	project1ID := createProject(e, uID, map[string]any{
+		"name":        "Project 1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	project2ID := createProject(e, uID, map[string]any{
+		"name":        "Project 2",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	project3ID := createProject(e, uID, map[string]any{
+		"name":        "Project 3",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	project4ID := createProject(e, uID, map[string]any{
+		"name":        "Project 4",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	project5ID := createProject(e, uID, map[string]any{
+		"name":        "Project 5",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+
+	updateStarProject(e, project1ID)
+	updateStarProject(e, project3ID)
 
 	// star and deleted 'Project 5'
-	starProject(e, project5ID)
+	updateStarProject(e, project5ID)
 	deleteProject(e, project5ID)
 
 	requestBody := GraphQLRequest{
@@ -175,6 +312,7 @@ func TestFindStarredByWorkspace(t *testing.T) {
 					id
 					name
 					starred
+					isDeleted
 				}
 				totalCount
 			}
@@ -184,8 +322,8 @@ func TestFindStarredByWorkspace(t *testing.T) {
 		},
 	}
 
-	starredProjects := callRequest(e, requestBody).
-		Value("starredProjects").Object()
+	starredProjects := Request(e, uID.String(), requestBody).
+		Path("$.data.starredProjects").Object()
 
 	totalCount := starredProjects.Value("totalCount").Raw()
 	assert.Equal(t, float64(2), totalCount, "Expected 2 starred projects")
@@ -222,122 +360,117 @@ func TestFindStarredByWorkspace(t *testing.T) {
 	assert.False(t, starredProjectsMap[project4ID], "Project 4 should not be starred")
 }
 
-func starProject(e *httpexpect.Expect, projectID string) {
-	updateProjectMutation := GraphQLRequest{
-		OperationName: "UpdateProject",
-		Query: `
-		mutation UpdateProject($projectId: ID!, $starred: Boolean!) {
-			updateProject(input: {projectId: $projectId, starred: $starred}) {
-				project {
-					id
-					starred
-				}
-			}
-		}`,
-		Variables: map[string]any{
+func updateStarProject(e *httpexpect.Expect, projectID string) {
+	response := updateProject(e, uID, map[string]any{
+		"input": map[string]any{
 			"projectId": projectID,
 			"starred":   true,
 		},
-	}
-
-	response := callRequest(e, updateProjectMutation).
-		Value("updateProject").Object().
-		Value("project").Object()
+	}).Object()
 
 	response.HasValue("id", projectID).
 		HasValue("starred", true)
 }
 
-const GetProjectsQuery = `
-query GetProjects($teamId: ID!, $pagination: Pagination, $keyword: String, $sort: ProjectSort) {
-  projects(
-    teamId: $teamId
-    pagination: $pagination
-    keyword: $keyword
-    sort: $sort
-  ) {
-    edges {
-      node {
-        id
-        ...ProjectFragment
-        scene {
-          id
-          __typename
-        }
-        __typename
-      }
-      __typename
-    }
-    pageInfo {
-      endCursor
-      hasNextPage
-      hasPreviousPage
-      startCursor
-      __typename
-    }
-    totalCount
-    __typename
-  }
-}
-fragment ProjectFragment on Project {
-  id
-  teamId
-  name
-  description
-  imageUrl
-  isArchived
-  isBasicAuthActive
-  basicAuthUsername
-  basicAuthPassword
-  publicTitle
-  publicDescription
-  publicImage
-  alias
-  enableGa
-  trackingId
-  publishmentStatus
-  updatedAt
-  createdAt
-  coreSupport
-  starred
-  isDeleted
-  __typename
-}`
-
-func TestSortByUpdatedAt(t *testing.T) {
-
+// go test -v -run TestFindVisibilityProjects ./e2e/...
+func TestFindVisibilityProjects(t *testing.T) {
 	e := Server(t, baseSeeder)
+	project1ID := createProject(e, uID, map[string]any{
+		"name":        "Project 1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 
-	createProject(e, "project1-test")
-	project2ID := createProject(e, "project2-test")
-	createProject(e, "project3-test")
-
-	updateProjectMutation := GraphQLRequest{
-		OperationName: "UpdateProject",
-		Query: `mutation UpdateProject($input: UpdateProjectInput!) {
-			updateProject(input: $input) {
-				project {
-					id
-					name
-					description
-					updatedAt
-					__typename
-				}
-				__typename
-			}
-		}`,
+	requestBody := GraphQLRequest{
+		OperationName: "GetProjects",
+		Query:         GetProjectsQuery,
 		Variables: map[string]any{
-			"input": map[string]any{
-				"projectId":   project2ID,
-				"description": "test updaet",
+			"pagination": map[string]any{
+				"last": 5,
+			},
+			"teamId": wID.String(),
+			"sort": map[string]string{
+				"field":     "NAME",
+				"direction": "ASC",
 			},
 		},
 	}
 
+	// default private
+	edges := Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
+	edges.Value(1).Path("$.node.visibility").IsEqual("private")
+
+	// update public
+	updateVisibilityProject(e, project1ID)
+
+	// result public
+	edges = Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
+	edges.Value(1).Path("$.node.visibility").IsEqual("public")
+
+	// create public
+	createProject(e, uID, map[string]any{
+		"name":        "Project 1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+		"visibility":  "public",
+	})
+	edges = Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
+	edges.Value(2).Path("$.node.visibility").IsEqual("public")
+
+}
+
+func updateVisibilityProject(e *httpexpect.Expect, projectID string) {
+	response := updateProject(e, uID, map[string]any{
+		"input": map[string]any{
+			"projectId":  projectID,
+			"visibility": "public",
+		},
+	}).Object()
+
+	response.HasValue("id", projectID).
+		HasValue("visibility", "public")
+}
+
+func TestSortByUpdatedAt(t *testing.T) {
+
+	e := Server(t, baseSeeder)
+
+	createProject(e, uID, map[string]any{
+		"name":        "project1-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	project2ID := createProject(e, uID, map[string]any{
+		"name":        "project2-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "project3-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+
 	// Update 'project2'
-	callRequest(e, updateProjectMutation).
-		Value("updateProject").Object().
-		Value("project").Object()
+	updateProject(e, uID, map[string]any{
+		"input": map[string]any{
+			"projectId":   project2ID,
+			"description": "test updaet",
+		},
+	}).Object()
 
 	requestBody := GraphQLRequest{
 		OperationName: "GetProjects",
@@ -354,9 +487,8 @@ func TestSortByUpdatedAt(t *testing.T) {
 		},
 	}
 
-	edges := callRequest(e, requestBody).
-		Value("projects").Object().
-		Value("edges").Array()
+	edges := Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
 
 	edges.Length().IsEqual(3)
 	edges.Value(0).Object().Value("node").Object().Value("name").IsEqual("project2-test") // 'project2' is first
@@ -387,9 +519,9 @@ func TestDeleteProjects(t *testing.T) {
 			"teamId": wID,
 		},
 	}
-	nodes := callRequest(e, requestBody).
-		Value("deletedProjects").Object().
-		Value("nodes").Array()
+
+	nodes := Request(e, uID.String(), requestBody).
+		Path("$.data.deletedProjects.nodes").Array()
 
 	nodes.Length().IsEqual(1)
 	nodes.Value(0).Object().Value("name").IsEqual("test2-2")
@@ -404,117 +536,100 @@ func TestDeleteProjects(t *testing.T) {
 }
 
 func deleteProject(e *httpexpect.Expect, projectID string) {
-
-	updateProjectMutation := GraphQLRequest{
-		OperationName: "UpdateProject",
-		Query: `mutation UpdateProject($input: UpdateProjectInput!) {
-			updateProject(input: $input) {
-				project {
-					id
-					name
-					isDeleted
-					updatedAt
-					__typename
-				}
-				__typename
-			}
-		}`,
-		Variables: map[string]any{
-			"input": map[string]any{
-				"projectId": projectID,
-				"deleted":   true,
-			},
+	response := updateProject(e, uID, map[string]any{
+		"input": map[string]any{
+			"projectId": projectID,
+			"deleted":   true,
 		},
-	}
-
-	response := callRequest(e, updateProjectMutation).
-		Value("updateProject").Object().
-		Value("project").Object()
+	}).Object()
 
 	response.HasValue("id", projectID).
 		HasValue("isDeleted", true)
 }
 
-func createGraphQLRequest(name string, coreSupport bool) GraphQLRequest {
-	return GraphQLRequest{
-		OperationName: "CreateProject",
-		Query:         "mutation CreateProject($teamId: ID!, $visualizer: Visualizer!, $name: String!, $description: String!, $coreSupport: Boolean) {\n createProject(\n input: {teamId: $teamId, visualizer: $visualizer, name: $name, description: $description, coreSupport: $coreSupport}\n ) {\n project {\n id\n name\n description\n coreSupport\n __typename\n }\n __typename\n }\n}",
-		Variables: map[string]any{
-			"name":        name,
-			"description": "abc",
-			"teamId":      wID.String(),
-			"visualizer":  "CESIUM",
-			"coreSupport": coreSupport,
-		},
-	}
-}
-
-func callRequest(e *httpexpect.Expect, requestBody GraphQLRequest) *httpexpect.Object {
-	return Request(e, uID.String(), requestBody).
-		Object().
-		Value("data").Object()
-}
-
 func testData(e *httpexpect.Expect) {
 
 	// create coreSupport default(=false) project
-	requestBody := GraphQLRequest{
-		OperationName: "CreateProject",
-		Query:         "mutation CreateProject($teamId: ID!, $visualizer: Visualizer!, $name: String!, $description: String!, $coreSupport: Boolean) {\n  createProject(\n    input: {teamId: $teamId, visualizer: $visualizer, name: $name, description: $description, coreSupport: $coreSupport}\n  ) {\n    project {\n      id\n      name\n      description\n      coreSupport\n      __typename\n    }\n    __typename\n  }\n}",
-		Variables: map[string]any{
-			"name":        "test1-1",
-			"description": "abc",
-			"teamId":      wID.String(),
-			"visualizer":  "CESIUM",
-		},
-	}
-	callRequest(e, requestBody).Value("createProject").Object().Value("project").Object().
+	createProject2(e, uID, map[string]any{
+		"name":        "test1-1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+	}).Object().
 		HasValue("name", "test1-1").
-		HasValue("coreSupport", false)
-
-	// create coreSupport default(=false) `delete` project
-	requestBody = GraphQLRequest{
-		OperationName: "CreateProject",
-		Query:         "mutation CreateProject($teamId: ID!, $visualizer: Visualizer!, $name: String!, $description: String!) {\n createProject(\n input: {teamId: $teamId, visualizer: $visualizer, name: $name, description: $description}\n ) {\n project {\n id\n name\n description\n imageUrl\n coreSupport\n __typename\n }\n __typename\n }\n}",
-		Variables: map[string]any{
-			"name":        "test1-2",
-			"description": "abc",
-			"teamId":      wID.String(),
-			"visualizer":  "CESIUM",
-		},
-	}
-	id := callRequest(e, requestBody).Value("createProject").Object().Value("project").Object().
+		HasValue("coreSupport", false).   //default(=false)
+		HasValue("visibility", "private") //default(=private)
+
+	// create coreSupport default(=false) visibility default(=private) `delete` project
+	id := createProject2(e, uID, map[string]any{
+		"name":        "test1-2",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+	}).Object().
 		HasValue("name", "test1-2").
-		HasValue("coreSupport", false).
+		HasValue("coreSupport", false).    //default(=false)
+		HasValue("visibility", "private"). //default(=private)
 		Value("id").Raw().(string)
+
 	deleteProject(e, id) // delete
 
-	// create coreSupport:true project
-	requestBody = createGraphQLRequest("test2-1", true)
-	callRequest(e, requestBody).Value("createProject").Object().Value("project").Object().
+	// create coreSupport:true visibility:public
+	createProject2(e, uID, map[string]any{
+		"name":        "test2-1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+		"visibility":  "public",
+	}).Object().
 		HasValue("name", "test2-1").
-		HasValue("coreSupport", true)
-
-	// create coreSupport:true `delete` project
-	requestBody = createGraphQLRequest("test2-2", true)
-	id = callRequest(e, requestBody).Value("createProject").Object().Value("project").Object().
+		HasValue("coreSupport", true).
+		HasValue("visibility", "public")
+
+	// create coreSupport:true visibility:public `delete`
+	id = createProject2(e, uID, map[string]any{
+		"name":        "test2-2",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+		"visibility":  "public",
+	}).Object().
 		HasValue("name", "test2-2").
 		HasValue("coreSupport", true).
+		HasValue("visibility", "public").
 		Value("id").Raw().(string)
+
 	deleteProject(e, id) // delete
 
-	// create coreSupport:false project
-	requestBody = createGraphQLRequest("test3-1", false)
-	callRequest(e, requestBody).Value("createProject").Object().Value("project").Object().
+	// create coreSupport:false visibility:private
+	createProject2(e, uID, map[string]any{
+		"name":        "test3-1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": false,
+		"visibility":  "private",
+	}).Object().
 		HasValue("name", "test3-1").
-		HasValue("coreSupport", false)
-
-	// create coreSupport:false `delete` project
-	requestBody = createGraphQLRequest("test3-2", false)
-	id = callRequest(e, requestBody).Value("createProject").Object().Value("project").Object().
+		HasValue("coreSupport", false).
+		HasValue("visibility", "private")
+
+	// create coreSupport:false visibility:private `delete`
+	id = createProject2(e, uID, map[string]any{
+		"name":        "test3-2",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": false,
+		"visibility":  "private",
+	}).Object().
 		HasValue("name", "test3-2").
 		HasValue("coreSupport", false).
+		HasValue("visibility", "private").
 		Value("id").Raw().(string)
+
 	deleteProject(e, id) // delete
 }
 
@@ -546,8 +661,8 @@ func projectsOldData(t *testing.T, ctx context.Context, r *repo.Container, count
 			Workspace(wID).
 			Alias(alias).
 			IsArchived(false).
-			// Deleted(false).      not exist
-			// CoreSupport(true).   not exist
+			// Deleted(false). not exist
+			// CoreSupport(true). not exist
 			MustBuild()
 		err := r.Project.Save(ctx, p)
 		assert.Nil(t, err)
@@ -578,7 +693,8 @@ func TestGetProjectPagination(t *testing.T) {
 		},
 	}
 
-	projects := Request(e, uID.String(), requestBody).Object().Value("data").Object().Value("projects").Object()
+	projects := Request(e, uID.String(), requestBody).
+		Path("$.data.projects").Object()
 
 	projects.HasValue("totalCount", 21)
 
@@ -611,7 +727,9 @@ func TestGetProjectPagination(t *testing.T) {
 			// "keyword": "Project",
 		},
 	}
-	projects = Request(e, uID.String(), requestBody).Object().Value("data").Object().Value("projects").Object()
+	projects = Request(e, uID.String(), requestBody).
+		Path("$.data.projects").Object()
+
 	projects.HasValue("totalCount", 5)
 
 	edges = projects.Value("edges").Array().Iter()
@@ -659,7 +777,8 @@ func TestGetProjectPaginationKeyword(t *testing.T) {
 		},
 	}
 
-	projects := Request(e, uID.String(), requestBody).Object().Value("data").Object().Value("projects").Object()
+	projects := Request(e, uID.String(), requestBody).
+		Path("$.data.projects").Object()
 
 	projects.HasValue("totalCount", 20)
 
@@ -692,7 +811,9 @@ func TestGetProjectPaginationKeyword(t *testing.T) {
 			"keyword": "Project",
 		},
 	}
-	projects = Request(e, uID.String(), requestBody).Object().Value("data").Object().Value("projects").Object()
+	projects = Request(e, uID.String(), requestBody).
+		Path("$.data.projects").Object()
+
 	projects.HasValue("totalCount", 4)
 
 	edges = projects.Value("edges").Array().Iter()
@@ -706,3 +827,29 @@ func TestGetProjectPaginationKeyword(t *testing.T) {
 	pageInfo.HasValue("hasNextPage", false)
 
 }
+
+func TestProjectVisibility(t *testing.T) {
+	e := Server(t, baseSeeder)
+
+	createProject(e, uID, map[string]any{
+		"name":        "project1-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "project2-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "project3-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+}
diff --git a/server/e2e/gql_property_schema_test.go b/server/e2e/gql_property_schema_test.go
index b69f2f2427..9ffd6be3fc 100644
--- a/server/e2e/gql_property_schema_test.go
+++ b/server/e2e/gql_property_schema_test.go
@@ -9,7 +9,13 @@ import (
 
 func TestPropertySchemaOrder(t *testing.T) {
 	e := Server(t, baseSeeder)
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 	res := getScene(e, sId, language.Und.String())
 
diff --git a/server/e2e/gql_scene_test.go b/server/e2e/gql_scene_test.go
index 9896c1ca33..e2baa7f46f 100644
--- a/server/e2e/gql_scene_test.go
+++ b/server/e2e/gql_scene_test.go
@@ -63,7 +63,13 @@ func TestGetScenePlaceholderJapanese(t *testing.T) {
 
 func TestGetSceneNLSLayer(t *testing.T) {
 	e := Server(t, baseSeeder)
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 	_, _, lId := addNLSLayerSimple(e, sId, "someTitle99", 99)
 
@@ -77,15 +83,7 @@ func TestGetSceneNLSLayer(t *testing.T) {
 func createProjectWithExternalImage(e *httpexpect.Expect, name string) string {
 	requestBody := GraphQLRequest{
 		OperationName: "CreateProject",
-		Query: `mutation CreateProject($teamId: ID!, $visualizer: Visualizer!, $name: String!, $description: String!, $coreSupport: Boolean) {
-			createProject( input: {teamId: $teamId, visualizer: $visualizer, name: $name, description: $description, coreSupport: $coreSupport} ) { 
-				project { 
-					id
-					__typename 
-				} 
-				__typename 
-			}
-		}`,
+		Query:         CreateProjectMutation,
 		Variables: map[string]any{
 			"name":        name,
 			"description": "abc",
diff --git a/server/e2e/gql_storytelling_test.go b/server/e2e/gql_storytelling_test.go
index 3da14eb890..317d776f76 100644
--- a/server/e2e/gql_storytelling_test.go
+++ b/server/e2e/gql_storytelling_test.go
@@ -410,32 +410,6 @@ func TestStoryPublishing(t *testing.T) {
 
 }
 
-func createProject(e *httpexpect.Expect, name string) string {
-	requestBody := GraphQLRequest{
-		OperationName: "CreateProject",
-		Query: `mutation CreateProject($teamId: ID!, $visualizer: Visualizer!, $name: String!, $description: String!, $coreSupport: Boolean) {
-			createProject( input: {teamId: $teamId, visualizer: $visualizer, name: $name, description: $description, coreSupport: $coreSupport} ) { 
-				project { 
-					id
-					__typename 
-				} 
-				__typename 
-			}
-		}`,
-		Variables: map[string]any{
-			"name":        name,
-			"description": "abc",
-			"teamId":      wID.String(),
-			"visualizer":  "CESIUM",
-			"coreSupport": true,
-		},
-	}
-
-	res := Request(e, uID.String(), requestBody)
-
-	return res.Path("$.data.createProject.project.id").Raw().(string)
-}
-
 func createScene(e *httpexpect.Expect, pID string) (GraphQLRequest, *httpexpect.Value, string) {
 	requestBody := GraphQLRequest{
 		OperationName: "CreateScene",
diff --git a/server/e2e/gql_style_test.go b/server/e2e/gql_style_test.go
index aac8014c98..c5849b35cc 100644
--- a/server/e2e/gql_style_test.go
+++ b/server/e2e/gql_style_test.go
@@ -147,7 +147,13 @@ func fetchSceneForStyles(e *httpexpect.Expect, sID string) (GraphQLRequest, *htt
 func TestStyleCRUD(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 
 	// fetch scene
diff --git a/server/e2e/gql_validate_geojson_test.go b/server/e2e/gql_validate_geojson_test.go
index 92b047fce6..9d97265a14 100644
--- a/server/e2e/gql_validate_geojson_test.go
+++ b/server/e2e/gql_validate_geojson_test.go
@@ -8,7 +8,13 @@ func TestValidateGeoJsonOfAssets(t *testing.T) {
 	e := Server(t, baseSeeder)
 
 	teamId := wID.String()
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 
 	tests := []struct {
@@ -223,7 +229,14 @@ func TestValidateGeoJsonExternal(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			e := Server(t, baseSeeder)
-			pId := createProject(e, "test")
+			pId := createProject(e, uID, map[string]any{
+				"name":        "test",
+				"description": "abc",
+				"teamId":      wID.String(),
+				"visualizer":  "CESIUM",
+				"coreSupport": true,
+			})
+
 			_, _, sId := createScene(e, pId)
 			res := addNLSLayerSimpleByGeojson(e, sId, tt.url, "test", 0)
 			if tt.hasError {
@@ -238,7 +251,13 @@ func TestValidateGeoJsonExternal(t *testing.T) {
 func TestValidateGeoFormData(t *testing.T) {
 	e := Server(t, baseSeeder)
 
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 	_, _, sId := createScene(e, pId)
 	tests := []struct {
 		name     string
diff --git a/server/gql/project.graphql b/server/gql/project.graphql
index 21f02abbc3..a1a068f3c7 100644
--- a/server/gql/project.graphql
+++ b/server/gql/project.graphql
@@ -25,6 +25,7 @@ type Project implements Node {
   trackingId: String!
   starred: Boolean!
   isDeleted: Boolean!
+  visibility: String!
 }
 
 type ProjectAliasAvailability {
@@ -56,6 +57,7 @@ input CreateProjectInput {
   name: String
   description: String
   coreSupport: Boolean
+  visibility: String
 }
 
 input UpdateProjectInput {
@@ -79,6 +81,7 @@ input UpdateProjectInput {
   sceneId: ID
   starred: Boolean
   deleted: Boolean
+  visibility: String
 }
 
 input PublishProjectInput {
diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go
index de00d99f88..d5962ff0a7 100644
--- a/server/internal/adapter/gql/generated.go
+++ b/server/internal/adapter/gql/generated.go
@@ -556,6 +556,7 @@ type ComplexityRoot struct {
 		TeamID            func(childComplexity int) int
 		TrackingID        func(childComplexity int) int
 		UpdatedAt         func(childComplexity int) int
+		Visibility        func(childComplexity int) int
 		Visualizer        func(childComplexity int) int
 	}
 
@@ -3883,6 +3884,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Project.UpdatedAt(childComplexity), true
 
+	case "Project.visibility":
+		if e.complexity.Project.Visibility == nil {
+			break
+		}
+
+		return e.complexity.Project.Visibility(childComplexity), true
+
 	case "Project.visualizer":
 		if e.complexity.Project.Visualizer == nil {
 			break
@@ -6764,6 +6772,7 @@ extend type Mutation {
   trackingId: String!
   starred: Boolean!
   isDeleted: Boolean!
+  visibility: String!
 }
 
 type ProjectAliasAvailability {
@@ -6795,6 +6804,7 @@ input CreateProjectInput {
   name: String
   description: String
   coreSupport: Boolean
+  visibility: String
 }
 
 input UpdateProjectInput {
@@ -6818,6 +6828,7 @@ input UpdateProjectInput {
   sceneId: ID
   starred: Boolean
   deleted: Boolean
+  visibility: String
 }
 
 input PublishProjectInput {
@@ -27536,6 +27547,50 @@ func (ec *executionContext) fieldContext_Project_isDeleted(_ context.Context, fi
 	return fc, nil
 }
 
+func (ec *executionContext) _Project_visibility(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Project) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Project_visibility(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Visibility, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Project_visibility(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Project",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _ProjectAliasAvailability_alias(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.ProjectAliasAvailability) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_ProjectAliasAvailability_alias(ctx, field)
 	if err != nil {
@@ -27765,6 +27820,8 @@ func (ec *executionContext) fieldContext_ProjectConnection_nodes(_ context.Conte
 				return ec.fieldContext_Project_starred(ctx, field)
 			case "isDeleted":
 				return ec.fieldContext_Project_isDeleted(ctx, field)
+			case "visibility":
+				return ec.fieldContext_Project_visibility(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type Project", field.Name)
 		},
@@ -28002,6 +28059,8 @@ func (ec *executionContext) fieldContext_ProjectEdge_node(_ context.Context, fie
 				return ec.fieldContext_Project_starred(ctx, field)
 			case "isDeleted":
 				return ec.fieldContext_Project_isDeleted(ctx, field)
+			case "visibility":
+				return ec.fieldContext_Project_visibility(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type Project", field.Name)
 		},
@@ -28100,6 +28159,8 @@ func (ec *executionContext) fieldContext_ProjectPayload_project(_ context.Contex
 				return ec.fieldContext_Project_starred(ctx, field)
 			case "isDeleted":
 				return ec.fieldContext_Project_isDeleted(ctx, field)
+			case "visibility":
+				return ec.fieldContext_Project_visibility(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type Project", field.Name)
 		},
@@ -34405,6 +34466,8 @@ func (ec *executionContext) fieldContext_Scene_project(_ context.Context, field
 				return ec.fieldContext_Project_starred(ctx, field)
 			case "isDeleted":
 				return ec.fieldContext_Project_isDeleted(ctx, field)
+			case "visibility":
+				return ec.fieldContext_Project_visibility(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type Project", field.Name)
 		},
@@ -44278,7 +44341,7 @@ func (ec *executionContext) unmarshalInputCreateProjectInput(ctx context.Context
 		asMap[k] = v
 	}
 
-	fieldsInOrder := [...]string{"teamId", "visualizer", "name", "description", "coreSupport"}
+	fieldsInOrder := [...]string{"teamId", "visualizer", "name", "description", "coreSupport", "visibility"}
 	for _, k := range fieldsInOrder {
 		v, ok := asMap[k]
 		if !ok {
@@ -44320,6 +44383,13 @@ func (ec *executionContext) unmarshalInputCreateProjectInput(ctx context.Context
 				return it, err
 			}
 			it.CoreSupport = data
+		case "visibility":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("visibility"))
+			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Visibility = data
 		}
 	}
 
@@ -46243,7 +46313,7 @@ func (ec *executionContext) unmarshalInputUpdateProjectInput(ctx context.Context
 		asMap[k] = v
 	}
 
-	fieldsInOrder := [...]string{"projectId", "name", "description", "archived", "isBasicAuthActive", "basicAuthUsername", "basicAuthPassword", "alias", "imageUrl", "publicTitle", "publicDescription", "publicImage", "publicNoIndex", "deleteImageUrl", "deletePublicImage", "enableGa", "trackingId", "sceneId", "starred", "deleted"}
+	fieldsInOrder := [...]string{"projectId", "name", "description", "archived", "isBasicAuthActive", "basicAuthUsername", "basicAuthPassword", "alias", "imageUrl", "publicTitle", "publicDescription", "publicImage", "publicNoIndex", "deleteImageUrl", "deletePublicImage", "enableGa", "trackingId", "sceneId", "starred", "deleted", "visibility"}
 	for _, k := range fieldsInOrder {
 		v, ok := asMap[k]
 		if !ok {
@@ -46390,6 +46460,13 @@ func (ec *executionContext) unmarshalInputUpdateProjectInput(ctx context.Context
 				return it, err
 			}
 			it.Deleted = data
+		case "visibility":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("visibility"))
+			data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Visibility = data
 		}
 	}
 
@@ -51617,6 +51694,11 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet,
 			if out.Values[i] == graphql.Null {
 				atomic.AddUint32(&out.Invalids, 1)
 			}
+		case "visibility":
+			out.Values[i] = ec._Project_visibility(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
diff --git a/server/internal/adapter/gql/gqlmodel/convert_project.go b/server/internal/adapter/gql/gqlmodel/convert_project.go
index 9736604442..970cb020eb 100644
--- a/server/internal/adapter/gql/gqlmodel/convert_project.go
+++ b/server/internal/adapter/gql/gqlmodel/convert_project.go
@@ -67,6 +67,7 @@ func ToProject(p *project.Project) *Project {
 		TrackingID:        p.TrackingID(),
 		Starred:           p.Starred(),
 		IsDeleted:         p.IsDeleted(),
+		Visibility:        p.Visibility(),
 	}
 }
 
diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go
index d4a51f4022..fef95b26f9 100644
--- a/server/internal/adapter/gql/gqlmodel/models_gen.go
+++ b/server/internal/adapter/gql/gqlmodel/models_gen.go
@@ -197,6 +197,7 @@ type CreateProjectInput struct {
 	Name        *string    `json:"name,omitempty"`
 	Description *string    `json:"description,omitempty"`
 	CoreSupport *bool      `json:"coreSupport,omitempty"`
+	Visibility  *string    `json:"visibility,omitempty"`
 }
 
 type CreateSceneInput struct {
@@ -705,6 +706,7 @@ type Project struct {
 	TrackingID        string            `json:"trackingId"`
 	Starred           bool              `json:"starred"`
 	IsDeleted         bool              `json:"isDeleted"`
+	Visibility        string            `json:"visibility"`
 }
 
 func (Project) IsNode()        {}
@@ -1264,6 +1266,7 @@ type UpdateProjectInput struct {
 	SceneID           *ID      `json:"sceneId,omitempty"`
 	Starred           *bool    `json:"starred,omitempty"`
 	Deleted           *bool    `json:"deleted,omitempty"`
+	Visibility        *string  `json:"visibility,omitempty"`
 }
 
 type UpdatePropertyItemInput struct {
diff --git a/server/internal/adapter/gql/resolver_mutation_project.go b/server/internal/adapter/gql/resolver_mutation_project.go
index 858a4a56f2..67c356a092 100644
--- a/server/internal/adapter/gql/resolver_mutation_project.go
+++ b/server/internal/adapter/gql/resolver_mutation_project.go
@@ -34,6 +34,7 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input gqlmodel.Cre
 		Name:        input.Name,
 		Description: input.Description,
 		CoreSupport: input.CoreSupport,
+		Visibility:  input.Visibility,
 	}, getOperator(ctx))
 	if err != nil {
 		return nil, err
@@ -79,6 +80,7 @@ func (r *mutationResolver) UpdateProject(ctx context.Context, input gqlmodel.Upd
 		Starred:           input.Starred,
 		Deleted:           input.Deleted,
 		SceneID:           gqlmodel.ToIDRef[id.Scene](input.SceneID),
+		Visibility:        input.Visibility,
 	}, getOperator(ctx))
 	if err != nil {
 		return nil, err
diff --git a/server/internal/infrastructure/mongo/mongodoc/project.go b/server/internal/infrastructure/mongo/mongodoc/project.go
index a2677528dd..57b95d1c69 100644
--- a/server/internal/infrastructure/mongo/mongodoc/project.go
+++ b/server/internal/infrastructure/mongo/mongodoc/project.go
@@ -33,9 +33,9 @@ type ProjectDocument struct {
 	CoreSupport       bool
 	EnableGA          bool
 	TrackingID        string
-	// Scene             string
-	Starred bool
-	Deleted bool
+	Starred           bool
+	Deleted           bool
+	Visibility        string
 }
 
 type ProjectConsumer = Consumer[*ProjectDocument, *project.Project]
@@ -76,9 +76,9 @@ func NewProject(project *project.Project) (*ProjectDocument, string) {
 		CoreSupport:       project.CoreSupport(),
 		EnableGA:          project.EnableGA(),
 		TrackingID:        project.TrackingID(),
-		// Scene:             project.Scene().String(),
-		Starred: project.Starred(),
-		Deleted: project.IsDeleted(),
+		Starred:           project.Starred(),
+		Deleted:           project.IsDeleted(),
+		Visibility:        project.Visibility(),
 	}, pid
 }
 
@@ -126,8 +126,8 @@ func (d *ProjectDocument) Model() (*project.Project, error) {
 		CoreSupport(d.CoreSupport).
 		EnableGA(d.EnableGA).
 		TrackingID(d.TrackingID).
-		// Scene(scene).
 		Starred(d.Starred).
 		Deleted(d.Deleted).
+		Visibility(d.Visibility).
 		Build()
 }
diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go
index 540b947199..12f54615bd 100644
--- a/server/internal/usecase/interactor/project.go
+++ b/server/internal/usecase/interactor/project.go
@@ -98,6 +98,7 @@ func (i *Project) Create(ctx context.Context, input interfaces.CreateProjectPara
 		Name:        input.Name,
 		Description: input.Description,
 		CoreSupport: input.CoreSupport,
+		Visibility:  input.Visibility,
 	}, operator)
 }
 
@@ -213,6 +214,12 @@ func (i *Project) Update(ctx context.Context, p interfaces.UpdateProjectParam, o
 		}
 	}
 
+	if p.Visibility != nil {
+		if err := prj.UpdateVisibility(*p.Visibility); err != nil {
+			return nil, err
+		}
+	}
+
 	if len(graphql.GetErrors(ctx)) > 0 {
 		return prj, nil
 	}
@@ -663,6 +670,7 @@ type createProjectInput struct {
 	Alias       *string
 	Archived    *bool
 	CoreSupport *bool
+	Visibility  *string
 }
 
 func (i *Project) createProject(ctx context.Context, input createProjectInput, operator *usecase.Operator) (_ *project.Project, err error) {
@@ -735,6 +743,12 @@ func (i *Project) createProject(ctx context.Context, input createProjectInput, o
 		prj = prj.Name(*input.Name)
 	}
 
+	if input.Visibility != nil {
+		prj = prj.Visibility(*input.Visibility)
+	} else {
+		prj = prj.Visibility("private")
+	}
+
 	proj, err := prj.Build()
 	if err != nil {
 		return nil, err
diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go
index 8420ef2440..1fbe5d1729 100644
--- a/server/internal/usecase/interfaces/project.go
+++ b/server/internal/usecase/interfaces/project.go
@@ -24,6 +24,7 @@ type CreateProjectParam struct {
 	Name        *string
 	Description *string
 	CoreSupport *bool
+	Visibility  *string
 }
 
 type UpdateProjectParam struct {
@@ -47,6 +48,7 @@ type UpdateProjectParam struct {
 	SceneID           *id.SceneID
 	Starred           *bool
 	Deleted           *bool
+	Visibility        *string
 }
 
 type PublishProjectParam struct {
diff --git a/server/pkg/project/builder.go b/server/pkg/project/builder.go
index c28cc289f8..8068949bbb 100644
--- a/server/pkg/project/builder.go
+++ b/server/pkg/project/builder.go
@@ -83,6 +83,11 @@ func (b *Builder) Name(name string) *Builder {
 	return b
 }
 
+func (b *Builder) Visibility(visibility string) *Builder {
+	b.p.visibility = visibility
+	return b
+}
+
 func (b *Builder) Description(description string) *Builder {
 	b.p.description = description
 	return b
diff --git a/server/pkg/project/project.go b/server/pkg/project/project.go
index b95456936e..11a5db0b62 100644
--- a/server/pkg/project/project.go
+++ b/server/pkg/project/project.go
@@ -1,6 +1,7 @@
 package project
 
 import (
+	"errors"
 	"net/url"
 	"regexp"
 	"time"
@@ -54,6 +55,7 @@ type Project struct {
 	sceneId           id.SceneID
 	starred           bool
 	isDeleted         bool
+	visibility        string
 }
 
 func (p *Project) ID() id.ProjectID {
@@ -88,6 +90,10 @@ func (p *Project) Name() string {
 	return p.name
 }
 
+func (p *Project) Visibility() string {
+	return p.visibility
+}
+
 func (p *Project) Description() string {
 	return p.description
 }
@@ -207,6 +213,14 @@ func (p *Project) UpdateName(name string) {
 	p.name = name
 }
 
+func (p *Project) UpdateVisibility(visibility string) error {
+	if visibility != "public" && visibility != "private" {
+		return errors.New("visibility must be either 'public' or 'private'")
+	}
+	p.visibility = visibility
+	return nil
+}
+
 func (p *Project) UpdateDescription(description string) {
 	p.description = description
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (11)</summary><blockquote>

<details>
<summary>server/e2e/gql_style_test.go (1)</summary><blockquote>

`150-156`: **Project creation refactored but missing visibility parameter.**

The project creation has been refactored to use a more detailed parameter map, but since this PR is adding visibility support, consider explicitly testing it.

```diff
 pId := createProject(e, uID, map[string]any{
 	"name":        "test",
 	"description": "abc",
 	"teamId":      wID.String(),
 	"visualizer":  "CESIUM",
 	"coreSupport": true,
+	"visibility":  "private", // Explicitly set visibility to test the new feature
 })
```

</blockquote></details>
<details>
<summary>server/internal/usecase/interfaces/project.go (1)</summary><blockquote>

`51-51`: **Consider adding documentation about allowed values.**

Since visibility appears to be an enum-like value (public/private), consider adding a comment or validation to clarify the expected values.

```diff
+	// Visibility can be "public" or "private". If nil, the visibility remains unchanged.
 	Visibility        *string
```

</blockquote></details>
<details>
<summary>server/pkg/project/project.go (1)</summary><blockquote>

`92-217`: **Consider adding documentation for the visibility field.**

Adding comments to document the purpose and allowed values of the visibility field would improve code maintainability.


```diff
+// Visibility returns the current visibility setting of the project.
+// Valid values are "public" and "private".
 func (p *Project) Visibility() string {
 	return p.visibility
 }

+// UpdateVisibility sets the visibility of the project.
+// Visibility controls whether the project is accessible to users outside the workspace.
+// Valid values are "public" (accessible to anyone) and "private" (accessible only to workspace members).
 func (p *Project) UpdateVisibility(visibility string) {
 	p.visibility = visibility
 }
```

</blockquote></details>
<details>
<summary>server/e2e/gql_scene_test.go (1)</summary><blockquote>

`64-80`: **Consider adding a test for the visibility field.**

While the test infrastructure has been updated to support the visibility field, there are no tests that specifically verify the functionality of this new field. Consider adding a test that creates projects with different visibility settings and verifies the expected behavior.


```diff
func TestProjectVisibility(t *testing.T) {
	e := Server(t, baseSeeder)
	
	// Create a private project (default)
	privateId := createProject(e, uID, map[string]any{
		"name":        "private project",
		"description": "abc",
		"teamId":      wID.String(),
		"visualizer":  "CESIUM",
		"coreSupport": true,
	})
	
	// Create a public project
	publicId := createProject(e, uID, map[string]any{
		"name":        "public project",
		"description": "abc",
		"teamId":      wID.String(),
		"visualizer":  "CESIUM",
		"coreSupport": true,
		"visibility":  "public",
	})
	
	// Verify project visibility
	privateProject := getProject(e, privateId)
	privateProject.Object().Value("visibility").IsEqual("private")
	
	publicProject := getProject(e, publicId)
	publicProject.Object().Value("visibility").IsEqual("public")
	
	// Update project visibility
	_, _ = updateProject(e, privateId, map[string]any{
		"visibility": "public",
	})
	
	// Verify updated visibility
	updatedProject := getProject(e, privateId)
	updatedProject.Object().Value("visibility").IsEqual("public")
}
```

</blockquote></details>
<details>
<summary>server/gql/project.graphql (1)</summary><blockquote>

`28-28`: **Consider using an enum for visibility values**

While using a String type for visibility aligns with the PR requirements, consider using an enum type to restrict values to only "public" and "private" for better type safety. This would prevent invalid values from being used.


```diff
+enum ProjectVisibility {
+  PUBLIC
+  PRIVATE
+}

 type Project implements Node {
   ...
-  visibility: String!
+  visibility: ProjectVisibility!
 }

 input CreateProjectInput {
   ...
-  visibility: String
+  visibility: ProjectVisibility
 }

 input UpdateProjectInput {
   ...
-  visibility: String
+  visibility: ProjectVisibility
 }
```


Also applies to: 60-60, 84-84

</blockquote></details>
<details>
<summary>server/e2e/gql_validate_geojson_test.go (1)</summary><blockquote>

`232-238`: **Consider adding a test for the new visibility field.**

While the refactored `createProject` calls include many project attributes, none explicitly set the visibility parameter that's been added in this PR. Consider adding a test case that verifies both "public" and "private" visibility settings.

```diff
 pId := createProject(e, uID, map[string]any{
 	"name":        "test",
 	"description": "abc",
 	"teamId":      wID.String(),
 	"visualizer":  "CESIUM",
 	"coreSupport": true,
+	"visibility":  "public",
 })
```

</blockquote></details>
<details>
<summary>server/internal/usecase/interactor/project.go (1)</summary><blockquote>

`744-748`: **Default visibility value is set correctly**

The code correctly sets the visibility value during project creation, with "private" as the default when no visibility is specified.


Consider using constants for visibility values instead of string literals for better maintainability. For example:

```diff
-	if input.Visibility != nil {
-		prj = prj.Visibility(*input.Visibility)
-	} else {
-		prj = prj.Visibility("private")
-	}
+	const (
+		VisibilityPrivate = "private"
+		VisibilityPublic  = "public"
+	)
+
+	if input.Visibility != nil {
+		prj = prj.Visibility(*input.Visibility)
+	} else {
+		prj = prj.Visibility(VisibilityPrivate)
+	}
```

Alternatively, if these constants already exist elsewhere in the codebase, import and use them here.

</blockquote></details>
<details>
<summary>server/e2e/gql_project_test.go (4)</summary><blockquote>

`19-47`: **Extract common GraphQL fragments to avoid duplication & drift**

`ProjectFragment` is hard‑coded in this file, but the exact same fragment (or something very close) appears in several other E2E test files.  When the schema inevitably changes again you’ll have to hunt every copy down and keep them in sync, which is error‑prone.

A low‑effort win is to move the fragment into a small helper (e.g. `server/e2e/gql_fragments.go`) and import/concatenate it from all tests.  
That keeps the contract with the schema in a single place and avoids silent test breakage when fields are renamed or removed.

---

`124-142`: **Unify helpers & surface GraphQL errors**

`createProject` and `createProject2` differ only in the return value (ID vs. full node).  
Having two nearly‑identical helpers invites accidental misuse and extra maintenance.

You can collapse them into a single helper that returns the full JSON object; callers that need only the ID can chain `.Value("id").String().Raw()`.

At the same time, none of the helpers assert that `errors` is absent in the GraphQL response.  A silent server‑side failure would currently pass the tests and cause a panic later when `.Raw().(string)` is executed on `null`.

```diff
- res := Request(e, u.String(), requestBody)
- return res.Path("$.data.createProject.project.id").Raw().(string)
+ res := Request(e, u.String(), requestBody)
+ res.NotContainsKey("errors")      // fail fast if the mutation failed
+ return res.Path("$.data.createProject.project") // let caller pick fields
```

---

`156-164`: **Assert absence of GraphQL errors in `updateProject`**

Same concern as above: if the resolver returns an error, the tests will continue with an empty object and may mask real regressions.  
Add an explicit `NotContainsKey("errors")` (or similar) before dereferencing the `project` path.

---

`468-474`: **Minor typo in test data**

`"test updaet"` looks like a typo; if intentional, please add a comment, otherwise consider correcting to improve readability.

```diff
-            "description": "test updaet",
+            "description": "test update",
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 42585f128fb44a22839666b44012b877364e0735 and af09d001796cb858543637f9c6ba619e4a5d410f.

</details>

<details>
<summary>📒 Files selected for processing (20)</summary>

* `server/e2e/gql_asset_test.go` (1 hunks)
* `server/e2e/gql_custom_property_test.go` (3 hunks)
* `server/e2e/gql_featureCollection_test.go` (1 hunks)
* `server/e2e/gql_nlslayer_test.go` (5 hunks)
* `server/e2e/gql_project_test.go` (18 hunks)
* `server/e2e/gql_property_schema_test.go` (1 hunks)
* `server/e2e/gql_scene_test.go` (2 hunks)
* `server/e2e/gql_storytelling_test.go` (0 hunks)
* `server/e2e/gql_style_test.go` (1 hunks)
* `server/e2e/gql_validate_geojson_test.go` (3 hunks)
* `server/gql/project.graphql` (3 hunks)
* `server/internal/adapter/gql/generated.go` (15 hunks)
* `server/internal/adapter/gql/gqlmodel/convert_project.go` (1 hunks)
* `server/internal/adapter/gql/gqlmodel/models_gen.go` (3 hunks)
* `server/internal/adapter/gql/resolver_mutation_project.go` (2 hunks)
* `server/internal/infrastructure/mongo/mongodoc/project.go` (3 hunks)
* `server/internal/usecase/interactor/project.go` (4 hunks)
* `server/internal/usecase/interfaces/project.go` (2 hunks)
* `server/pkg/project/builder.go` (1 hunks)
* `server/pkg/project/project.go` (3 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* server/e2e/gql_storytelling_test.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>server/e2e/gql_project_test.go (1)</summary>

```
Learnt from: hexaforce
PR: reearth/reearth-visualizer#1141
File: server/e2e/gql_import_export_test.go:48-80
Timestamp: 2024-09-13T01:19:11.267Z
Learning: In `server/e2e/gql_import_export_test.go`, test functions like `TestCallImportProject` may have disabled assertions due to problems with GraphQL.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>server/e2e/gql_project_test.go (2)</summary><blockquote>

<details>
<summary>server/e2e/common.go (2)</summary>

* `GraphQLRequest` (186-190)
* `Request` (192-203)

</details>
<details>
<summary>web/src/services/gql/__gen__/graphql.ts (4)</summary>

* `Query` (1465-1481)
* `CreateProjectMutation` (2563-2563)
* `UpdateProjectMutation` (2582-2582)
* `GetProjectsQuery` (2545-2545)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: deploy-server / deploy_test

</details>

<details>
<summary>🔇 Additional comments (36)</summary><blockquote>

<details>
<summary>server/pkg/project/builder.go (1)</summary>

`86-89`: **Builder method for visibility looks good!**

The `Visibility` method follows the builder pattern consistently with other methods in this file, allowing for fluent method chaining.

</details>
<details>
<summary>server/internal/adapter/gql/gqlmodel/convert_project.go (1)</summary>

`70-70`: **Field mapping for visibility looks correct.**

The `Visibility` field is correctly mapped from the domain model to the GraphQL model, consistent with how other fields are handled.

</details>
<details>
<summary>server/internal/usecase/interfaces/project.go (1)</summary>

`27-27`: **Field type is appropriate for optional visibility.**

Using `*string` is correct for an optional parameter, allowing distinction between unset and empty values.

</details>
<details>
<summary>server/internal/adapter/gql/resolver_mutation_project.go (2)</summary>

`37-37`: **Implementation of visibility parameter in CreateProject looks good.**

The visibility field is correctly passed from the GraphQL input to the CreateProjectParam structure, enabling the creation of projects with a specific visibility setting.

---

`83-83`: **Implementation of visibility parameter in UpdateProject looks good.**

The visibility field is correctly passed from the GraphQL input to the UpdateProjectParam structure, enabling the modification of a project's visibility setting.

</details>
<details>
<summary>server/e2e/gql_nlslayer_test.go (5)</summary>

`388-394`: **Test data enrichment for project creation looks good.**

The test has been updated to create projects with richer metadata, including the teamId, visualizer, and coreSupport fields, aligning with the broader standardization of project creation in tests.

---

`781-787`: **Test data enrichment for project creation is consistent.**

The test data is consistent with the first instance, creating projects with the same set of enriched parameters.

---

`859-865`: **Test data enrichment for project creation maintains consistency.**

The project creation parameters remain consistent throughout the test file, which is good for maintainability.

---

`917-923`: **Test data enrichment for project creation maintains consistency.**

The consistent use of the same project creation pattern across multiple tests demonstrates good refactoring practices.

---

`991-997`: **Test data enrichment for project creation maintains consistency.**

Final instance of project creation follows the same pattern, completing the consistent refactoring across all tests.

</details>
<details>
<summary>server/pkg/project/project.go (2)</summary>

`57-57`: **Visibility field added to Project struct.**

The visibility field has been added to the Project struct, which is a foundational change for the public projects feature.

---

`92-94`: **Visibility getter method looks good.**

The Visibility() method follows the established pattern in the codebase for accessor methods.

</details>
<details>
<summary>server/e2e/gql_scene_test.go (2)</summary>

`66-72`: **Test data enrichment for project creation looks good.**

The test has been updated to create projects with richer metadata, consistent with changes in other test files.

---

`86-86`: **Good refactoring to use a shared GraphQL mutation constant.**

Using the shared CreateProjectMutation constant improves maintainability by centralizing the GraphQL mutation definition.

</details>
<details>
<summary>server/e2e/gql_asset_test.go (2)</summary>

`129-135`: **Improved project creation with comprehensive attributes**

The change enhances the project creation by using a structured map instead of just a project name, which supports the new visibility attribute and aligns with the refactoring of project creation across tests.

---

`139-145`: **Consistent project creation pattern applied**

This follows the same improved pattern as the previous project creation, ensuring consistency across the test suite.

</details>
<details>
<summary>server/e2e/gql_property_schema_test.go (1)</summary>

`12-18`: **Refactored project creation with detailed attributes**

The project creation has been updated to use a structured map with detailed attributes, following the consistent pattern introduced in this PR.

</details>
<details>
<summary>server/gql/project.graphql (3)</summary>

`28-28`: **Added visibility field to Project type**

The visibility field has been added to the Project type as required by the PR objectives. This is the core change that enables public/private project support.

---

`60-60`: **Added optional visibility parameter to CreateProjectInput**

The visibility field has been correctly added as an optional parameter to the CreateProjectInput type, which aligns with the requirement that it should default to "private" if not specified.

---

`84-84`: **Added visibility parameter to UpdateProjectInput**

The visibility field has been added to the UpdateProjectInput type, allowing projects to have their visibility updated after creation.

</details>
<details>
<summary>server/e2e/gql_custom_property_test.go (3)</summary>

`13-19`: **Updated project creation with comprehensive attributes**

The project creation has been refactored to use a map with detailed attributes, supporting the new visibility feature and maintaining consistency with other test files.

---

`110-116`: **Consistent project creation pattern**

This change follows the same pattern as the previous project creation, ensuring uniformity across test files.

---

`237-243`: **Standardized project creation approach**

The project creation follows the consistent pattern established in this PR, improving maintainability of tests.

</details>
<details>
<summary>server/e2e/gql_validate_geojson_test.go (2)</summary>

`11-17`: **Updated project creation pattern looks good.**

The changes are part of a broader standardization across test files to create projects with richer metadata, which improves test consistency and readability.

---

`254-260`: **Consistent project creation pattern applied.**

The standardized project creation pattern is consistently applied throughout the test file, which is good for maintainability.

</details>
<details>
<summary>server/e2e/gql_featureCollection_test.go (1)</summary>

`179-185`: **Refactored project creation pattern looks good.**

The change is consistent with the standardization across test files to use a more explicit and detailed project creation approach with richer metadata.

</details>
<details>
<summary>server/internal/adapter/gql/gqlmodel/models_gen.go (3)</summary>

`200-200`: **Properly added visibility field to CreateProjectInput.**

The visibility field is correctly implemented as an optional string in the input type, which aligns with the requirement that it should default to "private" if not specified.

---

`709-709`: **Properly implemented non-nullable visibility field in Project type.**

The visibility field is correctly implemented as a non-nullable string in the Project struct, ensuring it will always be present in the response.

---

`1269-1269`: **Added visibility field to UpdateProjectInput.**

The visibility field is correctly implemented as an optional string in the update input type, allowing users to modify the visibility setting of existing projects.

</details>
<details>
<summary>server/internal/infrastructure/mongo/mongodoc/project.go (3)</summary>

`38-38`: **Properly added visibility field to ProjectDocument.**

The visibility field is correctly added to the MongoDB document structure.

---

`79-81`: **Added visibility to document conversion.**

The changes properly map the project's visibility from the domain model to the MongoDB document.

---

`131-131`: **Added visibility to model reconstruction.**

The changes properly map the visibility field from the MongoDB document back to the domain model using the builder pattern.

</details>
<details>
<summary>server/internal/usecase/interactor/project.go (2)</summary>

`217-219`: **LGTM! Visibility update handler added appropriately**

The Update method now properly handles the visibility parameter when provided, consistent with how other fields are updated.

---

`671-671`: **LGTM! Struct field added correctly**

The Visibility field has been properly added to the createProjectInput struct with the appropriate pointer type, matching the pattern used for other optional fields.

</details>
<details>
<summary>server/internal/adapter/gql/generated.go (1)</summary>

`556-560`: **GraphQL schema modifications for project visibility look correct**

The added visibility field has been properly integrated into the GraphQL schema, resolvers, and related utilities. The changes include:

1. Adding complexity calculation support for the visibility field
2. Defining visibility as a non-nullable String in the Project type
3. Adding optional visibility parameters to CreateProjectInput and UpdateProjectInput
4. Implementing field resolvers and context handlers for the visibility field
5. Updating marshaling/unmarshaling code to handle the new field

These changes align well with the PR objective of introducing project visibility control.



Also applies to: 3884-3894, 6772-6776, 6804-6808, 6828-6832, 27547-27594, 27820-27825, 28059-28064, 28159-28164, 34466-34471, 44341-44345, 44383-44393, 46313-46317, 46460-46470, 51694-51702

</details>
<details>
<summary>server/e2e/gql_project_test.go (1)</summary>

`96-113`: <details>
<summary>❓ Verification inconclusive</summary>

**Confirm `visibility` variable type matches the updated schema**

`$visibility` is declared as `String` in the mutation, yet the domain model treats it as an enum‑like value (`"public" | "private"`).  
If the GraphQL schema was defined as a custom enum (`Visibility!`) instead of a plain string, this mutation will fail at runtime.


Run the quick grep below to double‑check the schema:


Expect the field to read `visibility: String` (not `Visibility`).  If it is an enum, update the variable declaration accordingly.

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for the visibility field definition
rg -n --context 2 $'input CreateProjectInput|input UpdateProjectInput|type Project' | grep -n visibility
```

Length of output: 541

---

```bash
#!/bin/bash
# Search GraphQL schema files for CreateProjectInput and visibility definitions
rg -n 'input CreateProjectInput' -g '*.graphql' -A2
rg -n 'input UpdateProjectInput' -g '*.graphql' -A2
rg -n 'enum Visibility' -g '*.graphql'
rg -n 'visibility:' -g '*.graphql' -A1
```

---

```bash
#!/bin/bash
# Search GraphQL schema files for Create/Update inputs and Visibility enum definitions
rg -n 'input CreateProjectInput' -g '*.graphql' -A3
rg -n 'input UpdateProjectInput' -g '*.graphql' -A3
rg -n 'enum Visibility' -g '*.graphql'
rg -n 'visibility:' -g '*.graphql' -A1
```

---

```bash
#!/bin/bash
# Locate all GraphQL schema files
echo "Searching for .graphql and .gql files..."
find . -type f \( -name '*.graphql' -o -name '*.gql' \)

# If schema files are found, search them for CreateProjectInput and Visibility enum
echo "Inspecting schema for CreateProjectInput definition..."
find . -type f \( -name '*.graphql' -o -name '*.gql' \) -print0 \
  | xargs -0 rg -n 'input CreateProjectInput' -A3

echo "Inspecting schema for UpdateProjectInput definition..."
find . -type f \( -name '*.graphql' -o -name '*.gql' \) -print0 \
  | xargs -0 rg -n 'input UpdateProjectInput' -A3

echo "Looking for a Visibility enum..."
find . -type f \( -name '*.graphql' -o -name '*.gql' \) -print0 \
  | xargs -0 rg -n 'enum Visibility'

echo "Searching for visibility fields in schema..."
find . -type f \( -name '*.graphql' -o -name '*.gql' \) -print0 \
  | xargs -0 rg -n 'visibility:' -A1
```

---



</details>

**Ensure `visibility` variable type aligns with your GraphQL schema**

In your E2E test you declare:
```graphql
mutation CreateProject(
  … 
  $visibility: String
) { … }
```
but your domain treats `visibility` as an enum (`"public" | "private"`). If the schema defines it as:
```graphql
input CreateProjectInput {
  … 
  visibility: Visibility!
}
```
(or similarly in `UpdateProjectInput`), the test will fail at runtime.

• Check your SDL for `CreateProjectInput`/`UpdateProjectInput` and `enum Visibility` – if `visibility` is `Visibility!`, update the mutation to:
```diff
- $visibility: String
+ $visibility: Visibility!
```
• Otherwise (if the schema really uses `String`), no change is needed here.  
Please confirm and adjust the variable declaration as needed.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T06:43:33Z
user: login: pyshx
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:06:55Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T08:07:03Z
user: login: pyshx
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T08:07:05Z
user: login: hexaforce
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T08:11:47Z
user: login: pyshx
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T08:29:51Z

Review comments:
diff_hunk: @@ -207,6 +212,10 @@ func (p *Project) UpdateName(name string) {
 	p.name = name
 }
 
+func (p *Project) UpdateVisibility(visibility string) {
+	p.visibility = visibility
+}
path: server/pkg/project/project.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding validation to UpdateVisibility.**

The UpdateVisibility method accepts and sets any string value. Consider adding validation to ensure only valid visibility values ("public" or "private") are accepted.


```diff
-func (p *Project) UpdateVisibility(visibility string) {
-	p.visibility = visibility
+func (p *Project) UpdateVisibility(visibility string) error {
+	if visibility != "public" && visibility != "private" {
+		return errors.New("visibility must be either 'public' or 'private'")
+	}
+	p.visibility = visibility
+	return nil
}
```

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit 4fd7e4a

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-17T06:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,7 +10,13 @@ import (
 
 func TestUpdateCustomProperties(t *testing.T) {
 	e := Server(t, baseSeeder)
-	pId := createProject(e, "test")
+	pId := createProject(e, uID, map[string]any{
+		"name":        "test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
path: server/e2e/gql_custom_property_test.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing visibility parameter in test project creation**

While the project creation has been updated to support the new attributes, the visibility parameter is not being set in any of these test cases. Consider adding tests that explicitly set and verify the visibility attribute to ensure this new feature works as expected.


```diff
 pId := createProject(e, uID, map[string]any{
   "name":        "test",
   "description": "abc",
   "teamId":      wID.String(),
   "visualizer":  "CESIUM",
   "coreSupport": true,
+  "visibility":  "public", // Test with public visibility
 })
```


Also applies to: 110-116, 237-243

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T06:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,9 +33,9 @@ type ProjectDocument struct {
 	CoreSupport       bool
 	EnableGA          bool
 	TrackingID        string
-	// Scene             string
-	Starred bool
-	Deleted bool
+	Starred           bool
+	Deleted           bool
+	Visibility        string
path: server/internal/infrastructure/mongo/mongodoc/project.go
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify validation constraints for visibility values.**

The code adds the visibility field but doesn't show any validation to ensure only valid values ("public" or "private") are accepted. Consider adding validation at the domain model level.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if validation exists for visibility values in the domain model

echo "Searching for visibility validation in project domain model..."
rg -A 10 "Visibility\(" server/pkg/project/
```

Length of output: 1855

---



</details>

**Enforce allowed visibility values in the domain model**

The project’s `visibility` field is currently a plain string with no checks, so any value could be persisted. We should validate inputs at the domain level to only allow known visibility options (e.g., `"public"` or `"private"`).

Locations to update:
- server/pkg/project/project.go: `func (p *Project) UpdateVisibility(visibility string)`
- server/pkg/project/builder.go:    `func (b *Builder) Visibility(visibility string) *Builder`

Suggested change (example):
```go
func (p *Project) UpdateVisibility(vis string) error {
    switch vis {
    case "public", "private":
        p.visibility = vis
        return nil
    default:
        return fmt.Errorf("invalid visibility: %s", vis)
    }
}
```
And in the builder:
```go
func (b *Builder) Visibility(vis string) *Builder {
    if err := b.p.UpdateVisibility(vis); err != nil {
        // handle or propagate error
    }
    return b
}
```

This ensures only valid visibility values are ever set.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T06:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,6 +98,7 @@ func (i *Project) Create(ctx context.Context, input interfaces.CreateProjectPara
 		Name:        input.Name,
 		Description: input.Description,
 		CoreSupport: input.CoreSupport,
+		Visibility:  input.Visibility,
 	}, operator)
path: server/internal/usecase/interactor/project.go
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Implementation looks good, but visibility values should be validated**

The addition of the Visibility field to the Create method correctly passes the visibility parameter to the internal createProject function, which aligns with the PR objective of adding visibility support.


Since visibility should only be "public" or "private", consider adding validation. Run this script to check if validation exists elsewhere:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for any validation of visibility values in the codebase
rg -A 3 "visibility.*\b(public|private)\b" --type go
```

Length of output: 4691

---



</details>

**Add visibility validation in project creation**

There’s currently no check that `input.Visibility` is either `"public"` or `"private"` before it’s passed through. Please update the `Create` method in `server/internal/usecase/interactor/project.go` (around line 100) to enforce this, for example:

```go
func (i *Interactor) Create(ctx context.Context, input CreateProjectInput, operator User) (*Project, error) {
    // Validate visibility
    if input.Visibility != domain.VisibilityPublic && input.Visibility != domain.VisibilityPrivate {
        return nil, fmt.Errorf("invalid visibility %q: must be %q or %q", input.Visibility, domain.VisibilityPublic, domain.VisibilityPrivate)
    }

    // existing code…
    return createProject(ctx, project.CreateParams{
        CoreSupport: input.CoreSupport,
        Visibility:  input.Visibility,
    }, operator)
}
```

• File: server/internal/usecase/interactor/project.go  
• Location: start of the `Create` method (≈ line 100)

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T06:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -706,3 +827,29 @@ func TestGetProjectPaginationKeyword(t *testing.T) {
 	pageInfo.HasValue("hasNextPage", false)
 
 }
+
+func TestProjectVisibility(t *testing.T) {
+	e := Server(t, baseSeeder)
+
+	createProject(e, uID, map[string]any{
+		"name":        "project1-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "project2-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+	createProject(e, uID, map[string]any{
+		"name":        "project3-test",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
+}
path: server/e2e/gql_project_test.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Empty test does not assert anything**

`TestProjectVisibility` creates three projects but contains no assertions or expectations, so it will always pass even if visibility handling is broken.

Either implement the intended assertions (e.g. verify the default visibility is `"private"` and can be updated to `"public"`) or remove the test to avoid a false sense of coverage.

```diff
 func TestProjectVisibility(t *testing.T) {
     e := Server(t, baseSeeder)
@@
-    createProject(e, uID, map[string]any{ ... })
-    createProject(e, uID, map[string]any{ ... })
-    createProject(e, uID, map[string]any{ ... })
+    id := createProject(e, uID, map[string]any{
+        "teamId":      wID.String(),
+        "visualizer":  "CESIUM",
+    })
+
+    // default should be private
+    res := Request(e, uID.String(), GraphQLRequest{
+        OperationName: "GetProjects",
+        Query:         GetProjectsQuery,
+        Variables: map[string]any{
+            "teamId": wID.String(),
+        },
+    })
+    res.Path("$.data.projects.edges[0].node.visibility").IsEqual("private")
+
+    // update to public and re‑check
+    updateVisibilityProject(e, id)
+    res = Request(e, uID.String(), GraphQLRequest{ ... })
+    res.Path("$.data.projects.edges[0].node.visibility").IsEqual("public")
 }
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T06:43:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -222,122 +360,117 @@
 	assert.False(t, starredProjectsMap[project4ID], "Project 4 should not be starred")
 }
 
-func starProject(e *httpexpect.Expect, projectID string) {
-	updateProjectMutation := GraphQLRequest{
-		OperationName: "UpdateProject",
-		Query: `
-		mutation UpdateProject($projectId: ID!, $starred: Boolean!) {
-			updateProject(input: {projectId: $projectId, starred: $starred}) {
-				project {
-					id
-					starred
-				}
-			}
-		}`,
-		Variables: map[string]any{
+func updateStarProject(e *httpexpect.Expect, projectID string) {
+	response := updateProject(e, uID, map[string]any{
+		"input": map[string]any{
 			"projectId": projectID,
 			"starred":   true,
 		},
-	}
-
-	response := callRequest(e, updateProjectMutation).
-		Value("updateProject").Object().
-		Value("project").Object()
+	}).Object()
 
 	response.HasValue("id", projectID).
 		HasValue("starred", true)
 }
 
-const GetProjectsQuery = `
-query GetProjects($teamId: ID!, $pagination: Pagination, $keyword: String, $sort: ProjectSort) {
-  projects(
-    teamId: $teamId
-    pagination: $pagination
-    keyword: $keyword
-    sort: $sort
-  ) {
-    edges {
-      node {
-        id
-        ...ProjectFragment
-        scene {
-          id
-          __typename
-        }
-        __typename
-      }
-      __typename
-    }
-    pageInfo {
-      endCursor
-      hasNextPage
-      hasPreviousPage
-      startCursor
-      __typename
-    }
-    totalCount
-    __typename
-  }
-}
-fragment ProjectFragment on Project {
-  id
-  teamId
-  name
-  description
-  imageUrl
-  isArchived
-  isBasicAuthActive
-  basicAuthUsername
-  basicAuthPassword
-  publicTitle
-  publicDescription
-  publicImage
-  alias
-  enableGa
-  trackingId
-  publishmentStatus
-  updatedAt
-  createdAt
-  coreSupport
-  starred
-  isDeleted
-  __typename
-}`
-
-func TestSortByUpdatedAt(t *testing.T) {
-
+// go test -v -run TestFindVisibilityProjects ./e2e/...
+func TestFindVisibilityProjects(t *testing.T) {
 	e := Server(t, baseSeeder)
+	project1ID := createProject(e, uID, map[string]any{
+		"name":        "Project 1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+	})
 
-	createProject(e, "project1-test")
-	project2ID := createProject(e, "project2-test")
-	createProject(e, "project3-test")
-
-	updateProjectMutation := GraphQLRequest{
-		OperationName: "UpdateProject",
-		Query: `mutation UpdateProject($input: UpdateProjectInput!) {
-			updateProject(input: $input) {
-				project {
-					id
-					name
-					description
-					updatedAt
-					__typename
-				}
-				__typename
-			}
-		}`,
+	requestBody := GraphQLRequest{
+		OperationName: "GetProjects",
+		Query:         GetProjectsQuery,
 		Variables: map[string]any{
-			"input": map[string]any{
-				"projectId":   project2ID,
-				"description": "test updaet",
+			"pagination": map[string]any{
+				"last": 5,
+			},
+			"teamId": wID.String(),
+			"sort": map[string]string{
+				"field":     "NAME",
+				"direction": "ASC",
 			},
 		},
 	}
 
+	// default private
+	edges := Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
+	edges.Value(1).Path("$.node.visibility").IsEqual("private")
+
+	// update public
+	updateVisibilityProject(e, project1ID)
+
+	// result public
+	edges = Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
+	edges.Value(1).Path("$.node.visibility").IsEqual("public")
+
+	// create public
+	createProject(e, uID, map[string]any{
+		"name":        "Project 1",
+		"description": "abc",
+		"teamId":      wID.String(),
+		"visualizer":  "CESIUM",
+		"coreSupport": true,
+		"visibility":  "public",
+	})
+	edges = Request(e, uID.String(), requestBody).
+		Path("$.data.projects.edges").Array()
+	edges.Value(2).Path("$.node.visibility").IsEqual("public")
+
+}
path: server/e2e/gql_project_test.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Brittle index access may panic – assert length or search by ID**

`edges.Value(1)` and later `edges.Value(2)` are accessed without first ensuring the slice is large enough.  
If `baseSeeder` changes (or runs in a different order) the test will panic with “index out of range”.

Prefer checking the length first or, better, locate the element by its `id` so the test is order‑independent:

```diff
-edges.Value(1).Path("$.node.visibility").IsEqual("private")
+# ensure at least 2 projects
+edges.Length().Ge(2)
+
+# find the node for project1ID
+edges.Filter(func(e *httpexpect.Value) bool {
+    return e.Path("$.node.id").Raw() == project1ID
+}).First().Path("$.node.visibility").IsEqual("private")
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T06:43:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,6 +212,10 @@ func (p *Project) UpdateName(name string) {
 	p.name = name
 }
 
+func (p *Project) UpdateVisibility(visibility string) {
+	p.visibility = visibility
+}
path: server/pkg/project/project.go
user: login: pyshx
body: @hexaforce Please address this.
created_at: 2025-04-17T08:06:55Z
author_association: MEMBER
diff_hunk: @@ -207,6 +212,10 @@ func (p *Project) UpdateName(name string) {
 	p.name = name
 }
 
+func (p *Project) UpdateVisibility(visibility string) {
+	p.visibility = visibility
+}
path: server/pkg/project/project.go
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-04-17T08:07:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,6 +212,10 @@ func (p *Project) UpdateName(name string) {
 	p.name = name
 }
 
+func (p *Project) UpdateVisibility(visibility string) {
+	p.visibility = visibility
+}
path: server/pkg/project/project.go
user: login: hexaforce
body: OK
created_at: 2025-04-17T08:11:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-17T06:38:16Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce a new "visibility" attribute for projects across the backend, GraphQL schema, and end-to-end tests. The "visibility" field is added to the project domain model, GraphQL types, input structures, and MongoDB document representation. Corresponding builder and accessor methods are implemented in the domain layer. The GraphQL resolvers, schema, and generated code are updated to support querying, creating, and updating project visibility. End-to-end tests are refactored to create projects with detailed attributes, including visibility, and new helpers and query constants are introduced to streamline testing. Existing tests are updated to utilize these enhancements.

## Changes

| File(s)                                                                                     | Change Summary                                                                                                                                                                                                                                                                                                                                                                                         |
|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| server/pkg/project/project.go<br>server/pkg/project/builder.go                              | Added `visibility` field to `Project` struct, getter/setter methods, and a builder method for visibility.                                                                                                                                                                                                                                                       |
| server/internal/usecase/interfaces/project.go<br>server/internal/usecase/interactor/project.go | Added optional `Visibility *string` field to project creation/update parameters and logic to handle visibility in creation and update flows, defaulting to "private" if not specified.                                                                                                                                    |
| server/internal/infrastructure/mongo/mongodoc/project.go                                    | Added `Visibility` field to `ProjectDocument`, updated constructor and model conversion to handle visibility.                                                                                                                                                                                                            |
| server/internal/adapter/gql/gqlmodel/models_gen.go                                          | Added `Visibility` field to `Project`, `CreateProjectInput`, and `UpdateProjectInput` structs.                                                                                                                                                                                                                          |
| server/internal/adapter/gql/gqlmodel/convert_project.go                                     | Updated `ToProject` to map visibility from domain to GraphQL model.                                                                                                                                                                                                                                                     |
| server/internal/adapter/gql/generated.go                                                    | Added GraphQL support for `visibility`: schema, field resolver, input handling, and field context integration.                                                                                                                                                                                                          |
| server/internal/adapter/gql/resolver_mutation_project.go                                    | Passed `Visibility` from GraphQL input to use case layer in create/update mutations.                                                                                                                                                                                                                                    |
| server/gql/project.graphql                                                                  | Added `visibility: String!` to `Project` type, and optional `visibility` to input types in the GraphQL schema.                                                                                                                                                                                                         |
| server/e2e/gql_project_test.go                                                              | Major refactor: introduced query/mutation constants, helper functions for project creation/updating, added visibility-related tests, and updated all tests to use new helpers and detailed project attributes (including visibility).                                                                                    |
| server/e2e/gql_nlslayer_test.go<br>server/e2e/gql_custom_property_test.go<br>server/e2e/gql_asset_test.go<br>server/e2e/gql_featureCollection_test.go<br>server/e2e/gql_property_schema_test.go<br>server/e2e/gql_style_test.go<br>server/e2e/gql_validate_geojson_test.go<br>server/e2e/gql_scene_test.go | Updated project creation in tests to use detailed attribute maps (name, description, teamId, visualizer, coreSupport, and sometimes visibility) instead of just a name string.                                                                                                   |
| server/e2e/gql_storytelling_test.go                                                         | Removed the local `createProject` helper function.                                                                                                                                                                                                                                                                                                             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant GraphQL
    participant Usecase
    participant Domain
    participant MongoDB

    Client->>GraphQL: createProject(input: {name, ..., visibility})
    GraphQL->>Usecase: CreateProjectParam{..., Visibility}
    Usecase->>Domain: Builder().Visibility(visibility).Build()
    Domain->>MongoDB: Save ProjectDocument{..., Visibility}
    MongoDB-->>Domain: Persisted Project
    Domain-->>Usecase: Project instance
    Usecase-->>GraphQL: Project
    GraphQL-->>Client: Project{..., visibility}
```

```mermaid
sequenceDiagram
    participant Client
    participant GraphQL
    participant Usecase
    participant Domain
    participant MongoDB

    Client->>GraphQL: updateProject(input: {id, ..., visibility})
    GraphQL->>Usecase: UpdateProjectParam{..., Visibility}
    Usecase->>MongoDB: Fetch ProjectDocument
    MongoDB-->>Usecase: ProjectDocument
    Usecase->>Domain: Project.UpdateVisibility(visibility)
    Domain->>MongoDB: Update ProjectDocument{..., Visibility}
    MongoDB-->>Domain: Updated Project
    Domain-->>Usecase: Project instance
    Usecase-->>GraphQL: Project
    GraphQL-->>Client: Project{..., visibility}
```

## Poem

> A hop, a skip, a leap in code,  
> Now projects have a visibility mode!  
> From schema deep to Mongo's den,  
> The tests all dance and run again.  
> With helpers new and fields so bright,  
> The future’s clear—our projects light!  
> — 🐇✨

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a77c589fd554db2039e30686648687765437dad1 and 8c71ae5c5cf68092276faa64b9ff858a4f93a729.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `server/internal/usecase/interactor/project.go` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* server/internal/usecase/interactor/project.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: deploy-server / deploy_test

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=reearth/reearth-visualizer&utm_content=1573):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-04-17T06:38:27Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *reearth-web* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 8c71ae5c5cf68092276faa64b9ff858a4f93a729 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/reearth-web/deploys/6800b9a470d7b400083d1b74 |
author_association: NONE