Repository: reearth/reearth-visualizer
Pull request #1477: fix(server): project import export3 [VIZ-1360]
Author: login: hexaforce
Labels: name: server
Created: 2025-03-06T23:23:33Z | Merged: 2025-03-07T00:57:09Z

# Overview

## What I've done
### 1. I refactored ImportProject() because it was complicated.

### 2. I modified the asset file retrieval to perform a full scan of sceneJSON using a recursive loop.
This ensures that no asset files are missed and eliminates the need to fetch them individually.
```
func SearchAssetURL(...) {
    AddZipAsset(...)
}
```
### 3.Since tracking exported data was difficult, I added exportedInfo:
```
"exportedInfo": {
    "host": "http://localhost:8080",
    "project": "01jnpsd9sep7p00e7nsdd7g1f6",
    "timestamp": "2025-03-07T08:58:18+09:00"
}
```

## What I haven't done

## How I tested

## Which point I want you to review particularly

## Memo


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Streamlined project export/import processing for improved reliability and clearer error messaging.
  - Upgraded asset and plugin handling to support multi-file imports, enhancing data processing.
  - Simplified scene export operations for more efficient project updates.
  - Enhanced type safety by transitioning from `interface{}` to `any` in several method signatures.
  - Removed unnecessary complexity in asset management during project operations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/server/internal/adapter/gql/resolver_mutation_project.go b/server/internal/adapter/gql/resolver_mutation_project.go
index 1a44f30760..7280807103 100644
--- a/server/internal/adapter/gql/resolver_mutation_project.go
+++ b/server/internal/adapter/gql/resolver_mutation_project.go
@@ -7,20 +7,17 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"net/url"
 	"os"
-	"path"
 	"strings"
+	"time"
 
+	"github.com/reearth/reearth/server/internal/adapter"
 	"github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel"
 	"github.com/reearth/reearth/server/internal/usecase/interfaces"
-	"github.com/reearth/reearth/server/pkg/asset"
 	"github.com/reearth/reearth/server/pkg/file"
 	"github.com/reearth/reearth/server/pkg/id"
 	"github.com/reearth/reearth/server/pkg/visualizer"
 	"github.com/reearth/reearthx/account/accountdomain"
-	"github.com/reearth/reearthx/usecasex"
-	"github.com/samber/lo"
 	"github.com/spf13/afero"
 )
 
@@ -156,7 +153,7 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp
 		return nil, errors.New("Fail ExportProject :" + err.Error())
 	}
 
-	sce, data, err := usecases(ctx).Scene.ExportScene(ctx, prj, zipWriter)
+	sce, data, err := usecases(ctx).Scene.ExportScene(ctx, prj)
 	if err != nil {
 		return nil, errors.New("Fail ExportScene :" + err.Error())
 	}
@@ -166,34 +163,17 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp
 		return nil, errors.New("Fail ExportPlugins :" + err.Error())
 	}
 
-	page := usecasex.CursorPagination{
-		First: lo.ToPtr(int64(-1)),
-	}.Wrap()
-	sort := &asset.SortType{
-		Key:  "id",
-		Desc: true,
-	}
-
-	assets, _, err := usecases(ctx).Asset.FindByWorkspaceProject(ctx, prj.Workspace(), nil, nil, sort, page, getOperator(ctx))
-	if err != nil {
-		return nil, errors.New("Fail ExportAsset :" + err.Error())
-	}
-	assetNames := make(map[string]string)
-	for _, a := range gqlmodel.ToAssets(assets) {
-		parsedURL, err := url.Parse(a.URL)
-		if err != nil {
-			return nil, errors.New("Error parsing URL:" + err.Error())
-		}
-		fileName := path.Base(parsedURL.Path)
-		assetNames[fileName] = a.Name
-	}
-
 	data["project"] = gqlmodel.ToProjectExport(prj)
 	data["plugins"] = gqlmodel.ToPlugins(plgs)
 	data["schemas"] = gqlmodel.ToPropertySchemas(schemas)
-	data["assets"] = assetNames
 
-	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, data, prj)
+	exportedInfo := make(map[string]string)
+	exportedInfo["host"] = adapter.CurrentHost(ctx)
+	exportedInfo["project"] = prj.ID().String()
+	exportedInfo["timestamp"] = time.Now().Format(time.RFC3339)
+	data["exportedInfo"] = exportedInfo
+
+	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, Normalize(data), prj)
 	if err != nil {
 		return nil, errors.New("Fail UploadExportProjectZip :" + err.Error())
 	}
@@ -203,6 +183,16 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp
 	}, nil
 }
 
+func Normalize(data any) map[string]any {
+	if b, err := json.Marshal(data); err == nil {
+		var result map[string]any
+		if err := json.Unmarshal(b, &result); err == nil {
+			return result
+		}
+	}
+	return nil
+}
+
 func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.ImportProjectInput) (*gqlmodel.ImportProjectPayload, error) {
 
 	workspace, _ := accountdomain.WorkspaceIDFrom(string(input.TeamID))
@@ -215,119 +205,83 @@ func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.Imp
 	op := getOperator(ctx)
 
 	// First, create the project. A project associated with the asset is required.
-	projectData, _ := unmarshalProject(tempData)
-	prj, tx, err := usecases(ctx).Project.ImportProjectData(ctx, workspace, projectData, op)
+	newProject, tx, err := usecases(ctx).Project.ImportProjectData(ctx, workspace, tempData, op)
 	if err != nil {
-		return nil, errors.New("Fail ImportProject :" + err.Error())
+		return nil, errors.New("Fail ImportProjectData :" + err.Error())
 	}
+
 	defer func() {
 		if err2 := tx.End(ctx); err == nil && err2 != nil {
 			err = err2
 		}
 	}()
 
-	// If the project has an image, the old image path is set at this stage.
-	var oldProjectImage string
-	if prj.ImageURL() != nil {
-		oldProjectImage = path.Base(prj.ImageURL().Path)
-	}
-
-	pid, err := id.ProjectIDFrom(prj.ID().String())
+	tempData, err = usecases(ctx).Asset.ImportAssetFiles(ctx, assets, tempData, newProject)
 	if err != nil {
-		return nil, errors.New(err.Error())
+		return nil, errors.New("Fail ImportAssetFiles :" + err.Error())
 	}
 
-	// Assets file import
-	assetNames, _ := unmarshalAssets(tempData)
-	for beforeName, file := range assets {
-		realName := assetNames[beforeName]
-
-		// When an asset is registered, a new file URL is created.
-		url, _, err := usecases(ctx).Asset.ImportAssetFiles(ctx, realName, file, workspace, &pid)
-		if err != nil {
-			return nil, errors.New("Fail ImportAssetFiles :" + err.Error())
-		}
-
-		if oldProjectImage == beforeName {
-			prj, err = usecases(ctx).Project.Update(ctx, interfaces.UpdateProjectParam{
-				ID:       pid,
-				ImageURL: url,
-			}, getOperator(ctx))
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		afterName := path.Base(url.Path)
-		tempData = bytes.Replace(tempData, []byte(beforeName), []byte(afterName), -1)
+	newScene, err := usecases(ctx).Scene.Create(ctx, newProject.ID(), op)
+	if err != nil {
+		return nil, err
 	}
 
-	// need to create a Scene firstｓ
-	sce, err := usecases(ctx).Scene.Create(ctx, prj.ID(), op)
+	oldSceneID, err := getOldSceneID(tempData)
 	if err != nil {
 		return nil, err
 	}
-
-	newSceneID := sce.ID().String()
-
-	// Replace the SceneID with the new SceneID
-	oldSceneID, tempData, _ := replaceOldSceneID(tempData, newSceneID)
+	tempData = bytes.Replace(tempData, []byte(oldSceneID), []byte(newScene.ID().String()), -1)
 
 	// Plugin file upload
-	for fileName, file := range plugins {
-
-		parts := strings.Split(fileName, "/")
-		oldPID := parts[0]
-		fileName := parts[1]
-
-		// this is the name of the file path
-		newPluginId := strings.Replace(oldPID, oldSceneID, newSceneID, 1)
-
-		if err := usecases(ctx).Plugin.ImporPluginFile(ctx, newPluginId, fileName, file); err != nil {
-			return nil, errors.New("Fail ImporPluginFile :" + err.Error())
-		}
+	if err := usecases(ctx).Plugin.ImporPluginFile(ctx, plugins, oldSceneID, newScene.ID().String()); err != nil {
+		return nil, errors.New("Fail ImporPluginFile :" + err.Error())
 	}
 
-	pluginsData, sceneData, schemaData, _ := unmarshalPluginsScene(tempData)
+	pluginsData, sceneJsonData, schemaData, err := unmarshalPluginsScene(tempData)
+	if err != nil {
+		return nil, err
+	}
 
 	// Plugin data save
-	plgs, pss, err := usecases(ctx).Plugin.ImportPlugins(ctx, sce, pluginsData, schemaData)
+	plgs, pss, err := usecases(ctx).Plugin.ImportPlugins(ctx, newScene, pluginsData, schemaData)
 	if err != nil {
 		return nil, errors.New("Fail ImportPlugins :" + err.Error())
 	}
 
+	//　The following is the saving of sceneJSON. -----------------------
+
 	// Scene data save
-	sce, err = usecases(ctx).Scene.ImportScene(ctx, sce, prj, plgs, sceneData)
+	newScene, err = usecases(ctx).Scene.ImportScene(ctx, newScene, newProject, plgs, sceneJsonData)
 	if err != nil {
-		return nil, errors.New("Fail ImportScene :" + err.Error())
+		return nil, errors.New("Fail sceneJSON ImportScene :" + err.Error())
 	}
 
 	// Styles data save
-	styleList, replaceStyleIDs, err := usecases(ctx).Style.ImportStyles(ctx, sce.ID(), sceneData)
+	styleList, replaceStyleIDs, err := usecases(ctx).Style.ImportStyles(ctx, newScene.ID(), sceneJsonData)
 	if err != nil {
-		return nil, errors.New("Fail ImportStyles :" + err.Error())
+		return nil, errors.New("Fail sceneJSON ImportStyles :" + err.Error())
 	}
 
 	// NLSLayers data save
-	nlayers, replaceNLSLayerIDs, err := usecases(ctx).NLSLayer.ImportNLSLayers(ctx, sce.ID(), sceneData, replaceStyleIDs)
+	nlayers, replaceNLSLayerIDs, err := usecases(ctx).NLSLayer.ImportNLSLayers(ctx, newScene.ID(), sceneJsonData, replaceStyleIDs)
 	if err != nil {
-		return nil, errors.New("Fail ImportNLSLayers :" + err.Error())
+		return nil, errors.New("Fail sceneJSON ImportNLSLayers :" + err.Error())
 	}
 
 	// Story data save
-	st, err := usecases(ctx).StoryTelling.ImportStory(ctx, sce.ID(), sceneData, replaceNLSLayerIDs)
+	st, err := usecases(ctx).StoryTelling.ImportStory(ctx, newScene.ID(), sceneJsonData, replaceNLSLayerIDs)
 	if err != nil {
-		return nil, errors.New("Fail ImportStory :" + err.Error())
+		return nil, errors.New("Fail sceneJSON ImportStory :" + err.Error())
 	}
 
 	tx.Commit()
 
 	return &gqlmodel.ImportProjectPayload{
 		ProjectData: map[string]any{
-			"project":  gqlmodel.ToProject(prj),
+			"project":  gqlmodel.ToProject(newProject),
 			"plugins":  gqlmodel.ToPlugins(plgs),
 			"schema":   gqlmodel.ToPropertySchemas(pss),
-			"scene":    gqlmodel.ToScene(sce),
+			"scene":    gqlmodel.ToScene(newScene),
 			"nlsLayer": gqlmodel.ToNLSLayers(nlayers, nil),
 			"style":    gqlmodel.ToStyles(styleList),
 			"story":    gqlmodel.ToStory(st),
@@ -336,51 +290,27 @@ func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.Imp
 
 }
 
-func replaceOldSceneID(data []byte, newSceneID string) (string, []byte, error) {
-	var jsonData map[string]interface{}
-	if err := json.Unmarshal(data, &jsonData); err != nil {
-		return "", nil, err
-	}
-
-	oldSceneData, _ := jsonData["scene"].(map[string]interface{})
-	oldSceneID := oldSceneData["id"].(string)
-	return oldSceneID, bytes.Replace(data, []byte(oldSceneID), []byte(newSceneID), -1), nil
-}
-
-func unmarshalProject(data []byte) (map[string]interface{}, error) {
-	var jsonData map[string]interface{}
-	if err := json.Unmarshal(data, &jsonData); err != nil {
-		return nil, err
+func getOldSceneID(data []byte) (string, error) {
+	var d map[string]any
+	if err := json.Unmarshal(data, &d); err != nil {
+		return "", err
 	}
-
-	projectData, _ := jsonData["project"].(map[string]interface{})
-	return projectData, nil
-}
-
-func unmarshalAssets(data []byte) (map[string]string, error) {
-	var jsonData map[string]interface{}
-	if err := json.Unmarshal(data, &jsonData); err != nil {
-		return nil, err
-	}
-
-	assets, _ := jsonData["assets"].(map[string]interface{})
-	assetNames := make(map[string]string)
-	for k, v := range assets {
-		strValue, _ := v.(string)
-		assetNames[k] = strValue
+	if s, ok := d["scene"].(map[string]any); ok {
+		if id, ok := s["id"].(string); ok {
+			return id, nil
+		}
 	}
-
-	return assetNames, nil
+	return "", errors.New("scene id not found")
 }
 
-func unmarshalPluginsScene(data []byte) ([]interface{}, map[string]interface{}, []interface{}, error) {
-	var jsonData map[string]interface{}
-	if err := json.Unmarshal(data, &jsonData); err != nil {
+func unmarshalPluginsScene(data []byte) ([]any, map[string]any, []any, error) {
+	var d map[string]any
+	if err := json.Unmarshal(data, &d); err != nil {
 		return nil, nil, nil, err
 	}
 
-	pluginsData, _ := jsonData["plugins"].([]interface{})
-	sceneData, _ := jsonData["scene"].(map[string]interface{})
-	schemaData, _ := jsonData["schemas"].([]interface{})
+	pluginsData, _ := d["plugins"].([]any)
+	sceneData, _ := d["scene"].(map[string]any)
+	schemaData, _ := d["schemas"].([]any)
 	return pluginsData, sceneData, schemaData, nil
 }
diff --git a/server/internal/usecase/interactor/asset.go b/server/internal/usecase/interactor/asset.go
index 7afb9fa694..210e5541be 100644
--- a/server/internal/usecase/interactor/asset.go
+++ b/server/internal/usecase/interactor/asset.go
@@ -2,7 +2,9 @@ package interactor
 
 import (
 	"archive/zip"
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -15,8 +17,8 @@ import (
 	"github.com/reearth/reearth/server/pkg/asset"
 	"github.com/reearth/reearth/server/pkg/file"
 	"github.com/reearth/reearth/server/pkg/id"
+	"github.com/reearth/reearth/server/pkg/project"
 	"github.com/reearth/reearthx/account/accountdomain"
-	"github.com/reearth/reearthx/idx"
 	"github.com/reearth/reearthx/usecasex"
 )
 
@@ -151,45 +153,77 @@ func (i *Asset) Remove(ctx context.Context, aid id.AssetID, operator *usecase.Op
 	)
 }
 
-func (i *Asset) ImportAssetFiles(ctx context.Context, realName string, zipFile *zip.File, workspace idx.ID[accountdomain.Workspace], project *id.ProjectID) (*url.URL, int64, error) {
+func (i *Asset) ImportAssetFiles(ctx context.Context, assets map[string]*zip.File, data []byte, newProject *project.Project) ([]byte, error) {
 
-	readCloser, err := zipFile.Open()
-	if err != nil {
-		return nil, 0, fmt.Errorf("error opening zip file entry: %w", err)
+	var d map[string]any
+	if err := json.Unmarshal(data, &d); err != nil {
+		return nil, err
 	}
-	defer func() {
-		if cerr := readCloser.Close(); cerr != nil {
-			fmt.Printf("Error closing file: %v\n", cerr)
+
+	assetNames := make(map[string]string)
+	for beforeName, realName := range d["assets"].(map[string]any) {
+		if realName, ok := realName.(string); ok {
+			assetNames[beforeName] = realName
 		}
-	}()
-
-	contentType := http.DetectContentType([]byte(zipFile.Name))
-	file := &file.File{
-		Content:     readCloser,
-		Path:        realName,
-		Size:        int64(zipFile.UncompressedSize64),
-		ContentType: contentType,
-	}
-	url, size, err := i.gateways.File.UploadAsset(ctx, file)
-	if err != nil {
-		return nil, 0, err
 	}
 
-	a, err := asset.New().
-		NewID().
-		Workspace(workspace).
-		Project(project).
-		Name(path.Base(realName)).
-		Size(size).
-		URL(url.String()).
-		CoreSupport(true).
-		Build()
-	if err != nil {
-		return nil, 0, err
-	}
-	if err := i.repos.Asset.Save(ctx, a); err != nil {
-		return nil, 0, err
+	for beforeName, zipFile := range assets {
+		if zipFile.UncompressedSize64 == 0 {
+			continue
+		}
+		realName := assetNames[beforeName]
+		readCloser, err := zipFile.Open()
+		if err != nil {
+			return nil, err
+		}
+
+		defer func() {
+			if cerr := readCloser.Close(); cerr != nil {
+				fmt.Printf("Error closing file: %v\n", cerr)
+			}
+		}()
+
+		file := &file.File{
+			Content:     readCloser,
+			Path:        realName,
+			Size:        int64(zipFile.UncompressedSize64),
+			ContentType: http.DetectContentType([]byte(zipFile.Name)),
+		}
+
+		url, size, err := i.gateways.File.UploadAsset(ctx, file)
+		if err != nil {
+			return nil, err
+		}
+
+		if newProject.ImageURL() != nil {
+			if path.Base(newProject.ImageURL().Path) == beforeName {
+				newProject.SetImageURL(url)
+				err := i.repos.Project.Save(ctx, newProject)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+
+		a, err := asset.New().
+			NewID().
+			Workspace(newProject.Workspace()).
+			Project(newProject.ID().Ref()).
+			Name(path.Base(realName)).
+			Size(size).
+			URL(url.String()).
+			CoreSupport(true).
+			Build()
+		if err != nil {
+			return nil, err
+		}
+
+		if err := i.repos.Asset.Save(ctx, a); err != nil {
+			return nil, err
+		}
+		afterName := path.Base(url.Path)
+		data = bytes.Replace(data, []byte(beforeName), []byte(afterName), -1)
 	}
 
-	return url, size, err
+	return data, nil
 }
diff --git a/server/internal/usecase/interactor/common.go b/server/internal/usecase/interactor/common.go
index 50d5ae4302..7b3737fe2a 100644
--- a/server/internal/usecase/interactor/common.go
+++ b/server/internal/usecase/interactor/common.go
@@ -1,11 +1,8 @@
 package interactor
 
 import (
-	"archive/zip"
 	"context"
 	"errors"
-	"fmt"
-	"io"
 	"net/url"
 	"strings"
 
@@ -263,34 +260,3 @@ func ReplaceToCurrentHost(ctx context.Context, urlString string) string {
 	u.Host = u2.Host
 	return u.String()
 }
-
-// If the given path is the URL of an Asset, it will be added to the ZIP.
-func AddZipAsset(ctx context.Context, assetRepo repo.Asset, file gateway.File, zipWriter *zip.Writer, urlString string) error {
-
-	if !IsCurrentHostAssets(ctx, urlString) {
-		return nil
-	}
-
-	parts := strings.Split(urlString, "/")
-	fileName := parts[len(parts)-1]
-	stream, err := file.ReadAsset(ctx, fileName)
-	if err != nil {
-		return nil // skip if not available
-	}
-	defer func() {
-		if cerr := stream.Close(); cerr != nil {
-			fmt.Printf("Error closing file: %v\n", cerr)
-		}
-	}()
-	zipEntryPath := fmt.Sprintf("assets/%s", fileName)
-	zipEntry, err := zipWriter.Create(zipEntryPath)
-	if err != nil {
-		return err
-	}
-	_, err = io.Copy(zipEntry, stream)
-	if err != nil {
-		_ = stream.Close()
-		return err
-	}
-	return nil
-}
diff --git a/server/internal/usecase/interactor/plugin.go b/server/internal/usecase/interactor/plugin.go
index 30e7326d5f..d9051094be 100644
--- a/server/internal/usecase/interactor/plugin.go
+++ b/server/internal/usecase/interactor/plugin.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 
 	"github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel"
 	jsonmodel "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel"
@@ -181,7 +182,7 @@ func parsePropertySchemaField(fieldMap map[string]interface{}) *property.SchemaF
 	return fiBuilder.MustBuild()
 }
 
-func parseIsAvailableIf(conditionMap map[string]interface{}) *property.Condition {
+func parseIsAvailableIf(conditionMap map[string]any) *property.Condition {
 	fid := string(conditionMap["fieldId"].(string))
 	f := id.PropertyFieldIDFromRef(&fid)
 	t := gqlmodel.ToPropertyValueType(conditionMap["type"].(string))
@@ -192,7 +193,7 @@ func parseIsAvailableIf(conditionMap map[string]interface{}) *property.Condition
 	}
 }
 
-func parseSchemaFieldPointer(linkableFieldsMap map[string]interface{}) *property.SchemaFieldPointer {
+func parseSchemaFieldPointer(linkableFieldsMap map[string]any) *property.SchemaFieldPointer {
 	sg := linkableFieldsMap["schemaGroupId"].(string)
 	f := linkableFieldsMap["fieldId"].(string)
 	return &property.SchemaFieldPointer{
@@ -201,20 +202,20 @@ func parseSchemaFieldPointer(linkableFieldsMap map[string]interface{}) *property
 	}
 }
 
-func parsePropertySchema(psid id.PropertySchemaID, schemaMap map[string]interface{}) (*property.Schema, error) {
+func parsePropertySchema(psid id.PropertySchemaID, schemaMap map[string]any) (*property.Schema, error) {
 
-	groups := schemaMap["groups"].([]interface{})
+	groups := schemaMap["groups"].([]any)
 
 	// SchemaGroup -------------
 	sgl := make([]*property.SchemaGroup, 0)
 	for _, group := range groups {
-		groupMap := group.(map[string]interface{})
+		groupMap := group.(map[string]any)
 
 		// SchemaField -------------
 		fil := make([]*property.SchemaField, 0)
-		if fields, ok := groupMap["fields"].([]interface{}); ok {
+		if fields, ok := groupMap["fields"].([]any); ok {
 			for _, field := range fields {
-				fieldMap := field.(map[string]interface{})
+				fieldMap := field.(map[string]any)
 				fi := parsePropertySchemaField(fieldMap)
 				fil = append(fil, fi)
 			}
@@ -229,7 +230,7 @@ func parsePropertySchema(psid id.PropertySchemaID, schemaMap map[string]interfac
 		if v, ok := groupMap["isList"].(bool); ok {
 			sgBuilder = sgBuilder.IsList(v)
 		}
-		if v, ok := groupMap["isAvailableIf"].(map[string]interface{}); ok {
+		if v, ok := groupMap["isAvailableIf"].(map[string]any); ok {
 			sgBuilder = sgBuilder.IsAvailableIf(parseIsAvailableIf(v))
 		}
 		if v, ok := groupMap["title"].(string); ok {
@@ -249,13 +250,13 @@ func parsePropertySchema(psid id.PropertySchemaID, schemaMap map[string]interfac
 	}
 
 	// LinkableFields -------------
-	linkableFields := schemaMap["linkableFields"].(map[string]interface{})
+	linkableFields := schemaMap["linkableFields"].(map[string]any)
 
 	linkableBuilder := property.NewLinkableFields()
-	if v, ok := linkableFields["LatLng"].(map[string]interface{}); ok {
+	if v, ok := linkableFields["LatLng"].(map[string]any); ok {
 		linkableBuilder = linkableBuilder.LatLng(parseSchemaFieldPointer(v))
 	}
-	if v, ok := linkableFields["URL"].(map[string]interface{}); ok {
+	if v, ok := linkableFields["URL"].(map[string]any); ok {
 		linkableBuilder = linkableBuilder.URL(parseSchemaFieldPointer(v))
 	}
 	lf := linkableBuilder.MustBuild()
@@ -289,7 +290,7 @@ func parseWidgetLayout(model *jsonmodel.WidgetLayout) *plugin.WidgetLayout {
 	return &wl
 }
 
-func (i *Plugin) ImportPlugins(ctx context.Context, sce *scene.Scene, pluginsData []interface{}, schemasData []interface{}) ([]*plugin.Plugin, property.SchemaList, error) {
+func (i *Plugin) ImportPlugins(ctx context.Context, sce *scene.Scene, pluginsData []any, schemasData []any) ([]*plugin.Plugin, property.SchemaList, error) {
 	var pluginsJSON = jsonmodel.ToPluginsFromJSON(pluginsData)
 
 	readableFilter := repo.SceneFilter{Readable: scene.IDList{sce.ID()}}
@@ -327,16 +328,19 @@ func (i *Plugin) ImportPlugins(ctx context.Context, sce *scene.Scene, pluginsDat
 
 			// Save propertySchema
 			for _, schema := range schemasData {
-				schemaMap, _ := schema.(map[string]interface{})
-				if schemaMap["id"].(string) == psid.String() {
-					ps, err := parsePropertySchema(psid, schemaMap)
-					if err != nil {
-						return nil, nil, err
+				if schemaMap, ok := schema.(map[string]any); ok {
+					if id, ok := schemaMap["id"].(string); ok {
+						if id == psid.String() {
+							ps, err := parsePropertySchema(psid, schemaMap)
+							if err != nil {
+								return nil, nil, err
+							}
+							if err := i.propertySchemaRepo.Filtered(writableFilter).Save(ctx, ps); err != nil {
+								return nil, nil, errors.New("Save propertySchema :" + err.Error())
+							}
+							propertySchemaIDs = append(propertySchemaIDs, ps.ID())
+						}
 					}
-					if err := i.propertySchemaRepo.Filtered(writableFilter).Save(ctx, ps); err != nil {
-						return nil, nil, errors.New("Save propertySchema :" + err.Error())
-					}
-					propertySchemaIDs = append(propertySchemaIDs, ps.ID())
 				}
 			}
 		}
@@ -360,6 +364,7 @@ func (i *Plugin) ImportPlugins(ctx context.Context, sce *scene.Scene, pluginsDat
 		}
 	}
 
+	// check result
 	plgs, err := i.pluginRepo.Filtered(readableFilter).FindByIDs(ctx, pluginIDs)
 	if err != nil {
 		return nil, nil, err
@@ -372,34 +377,41 @@ func (i *Plugin) ImportPlugins(ctx context.Context, sce *scene.Scene, pluginsDat
 	return plgs, pss, nil
 }
 
-func (i *Plugin) ImporPluginFile(ctx context.Context, newPluginId string, name string, zipFile *zip.File) error {
+func (i *Plugin) ImporPluginFile(ctx context.Context, plugins map[string]*zip.File, oldSceneID string, newSceneID string) error {
 
-	pid, err := id.PluginIDFrom(newPluginId)
-	if err != nil {
-		return err
-	}
+	for fileName, zipFile := range plugins {
 
-	readCloser, err := zipFile.Open()
-	if err != nil {
-		return fmt.Errorf("error opening zip file entry: %w", err)
-	}
-	defer func() {
-		if cerr := readCloser.Close(); cerr != nil {
-			fmt.Printf("Error closing file: %v\n", cerr)
+		parts := strings.Split(fileName, "/")
+		oldPID := parts[0]
+		fileName := parts[1]
+
+		newPluginId := strings.Replace(oldPID, oldSceneID, newSceneID, 1)
+		pid, err := id.PluginIDFrom(newPluginId)
+		if err != nil {
+			return err
 		}
-	}()
 
-	contentType := http.DetectContentType([]byte(zipFile.Name))
+		readCloser, err := zipFile.Open()
+		if err != nil {
+			return fmt.Errorf("error opening zip file entry: %w", err)
+		}
+		defer func() {
+			if cerr := readCloser.Close(); cerr != nil {
+				fmt.Printf("Error closing file: %v\n", cerr)
+			}
+		}()
 
-	file := &file.File{
-		Content:     readCloser,
-		Path:        name,
-		Size:        int64(zipFile.UncompressedSize64),
-		ContentType: contentType,
-	}
+		file := &file.File{
+			Content:     readCloser,
+			Path:        fileName,
+			Size:        int64(zipFile.UncompressedSize64),
+			ContentType: http.DetectContentType([]byte(zipFile.Name)),
+		}
+
+		if err := i.file.UploadPluginFile(ctx, pid, file); err != nil {
+			return fmt.Errorf("error uploading plugin file: %w", err)
+		}
 
-	if err := i.file.UploadPluginFile(ctx, pid, file); err != nil {
-		return fmt.Errorf("error uploading plugin file: %w", err)
 	}
 
 	return nil
diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go
index bd37519268..1cdd788741 100644
--- a/server/internal/usecase/interactor/project.go
+++ b/server/internal/usecase/interactor/project.go
@@ -7,9 +7,13 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net/url"
+	"path"
+	"strings"
 	"time"
 
 	"github.com/99designs/gqlgen/graphql"
+	"github.com/reearth/reearth/server/internal/adapter"
 	jsonmodel "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel"
 	"github.com/reearth/reearth/server/internal/usecase"
 	"github.com/reearth/reearth/server/internal/usecase/gateway"
@@ -22,7 +26,6 @@ import (
 	"github.com/reearth/reearth/server/pkg/visualizer"
 	"github.com/reearth/reearthx/account/accountdomain"
 	"github.com/reearth/reearthx/account/accountusecase/accountrepo"
-	"github.com/reearth/reearthx/idx"
 	"github.com/reearth/reearthx/rerror"
 	"github.com/reearth/reearthx/usecasex"
 	"github.com/spf13/afero"
@@ -520,23 +523,97 @@ func (i *Project) ExportProjectData(ctx context.Context, projectID id.ProjectID,
 		return nil, errors.New("This project is deleted")
 	}
 
-	// project image
-	if prj.ImageURL() != nil {
-		err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, prj.ImageURL().Path)
-		if err != nil {
-			return nil, err
+	return prj, nil
+}
+
+func SearchAssetURL(ctx context.Context, data any, assetRepo repo.Asset, file gateway.File, zipWriter *zip.Writer, assetNames map[string]string) error {
+	switch v := data.(type) {
+	case map[string]any:
+		for _, value := range v {
+			if err := SearchAssetURL(ctx, value, assetRepo, file, zipWriter, assetNames); err != nil {
+				return err
+			}
 		}
+	case []any:
+		for _, item := range v {
+			if err := SearchAssetURL(ctx, item, assetRepo, file, zipWriter, assetNames); err != nil {
+				return err
+			}
+		}
+	case string:
+		if strings.HasPrefix(v, adapter.CurrentHost(ctx)) {
+			if err := AddZipAsset(ctx, assetRepo, file, zipWriter, v, assetNames); err != nil {
+				return err
+			}
+		}
+	default:
+
 	}
+	return nil
+}
 
-	return prj, nil
+// If the given path is the URL of an Asset, it will be added to the ZIP.
+func AddZipAsset(ctx context.Context, assetRepo repo.Asset, file gateway.File, zipWriter *zip.Writer, urlString string, assetNames map[string]string) error {
+
+	if !IsCurrentHostAssets(ctx, urlString) {
+		return nil
+	}
+
+	parts := strings.Split(urlString, "/")
+	beforeUniversaName := parts[len(parts)-1]
+	stream, err := file.ReadAsset(ctx, beforeUniversaName)
+	if err != nil {
+		return nil // skip if not available
+	}
+	defer func() {
+		if cerr := stream.Close(); cerr != nil {
+			fmt.Printf("Error closing file: %v\n", cerr)
+		}
+	}()
+
+	zipEntryPath := fmt.Sprintf("assets/%s", beforeUniversaName)
+	zipEntry, err := zipWriter.Create(zipEntryPath)
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(zipEntry, stream)
+	if err != nil {
+		_ = stream.Close()
+		return err
+	}
+	if a, err := assetRepo.FindByURL(ctx, urlString); a != nil && err == nil {
+		if parsedURL, err := url.Parse(urlString); err == nil {
+			assetNames[path.Base(parsedURL.Path)] = a.Name()
+		}
+	}
+
+	return nil
 }
 
 func (i *Project) UploadExportProjectZip(ctx context.Context, zipWriter *zip.Writer, zipFile afero.File, data map[string]interface{}, prj *project.Project) error {
+
+	assetNames := make(map[string]string)
+	if project, ok := data["project"].(map[string]interface{}); ok {
+		if imageUrl, ok := project["imageUrl"].(map[string]interface{}); ok {
+			if path, ok := imageUrl["Path"].(string); ok {
+				err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, adapter.CurrentHost(ctx)+path, assetNames)
+				if err != nil {
+					fmt.Printf("not notfound asset file: %v\n", err)
+				}
+			}
+		}
+	}
+	err := SearchAssetURL(ctx, data, i.assetRepo, i.file, zipWriter, assetNames)
+	if err != nil {
+		fmt.Printf("not notfound asset file: %v\n", err)
+	}
+	data["assets"] = assetNames
+
 	fileWriter, err := zipWriter.Create("project.json")
 	if err != nil {
 		return err
 	}
-	jsonData, err := json.Marshal(data)
+	jsonData, err := json.MarshalIndent(data, "", "  ")
 	if err != nil {
 		return err
 	}
@@ -562,13 +639,23 @@ func (i *Project) UploadExportProjectZip(ctx context.Context, zipWriter *zip.Wri
 	return nil
 }
 
-func (i *Project) ImportProjectData(ctx context.Context, workspace idx.ID[accountdomain.Workspace], projectData map[string]interface{}, op *usecase.Operator) (*project.Project, usecasex.Tx, error) {
+func (i *Project) ImportProjectData(ctx context.Context, workspace accountdomain.WorkspaceID, data []byte, op *usecase.Operator) (*project.Project, usecasex.Tx, error) {
 
 	tx, err := i.transaction.Begin(ctx)
 	if err != nil {
 		return nil, nil, err
 	}
 
+	var d map[string]any
+	if err := json.Unmarshal(data, &d); err != nil {
+		return nil, nil, err
+	}
+
+	projectData, ok := d["project"].(map[string]any)
+	if !ok {
+		return nil, nil, errors.New("project parse error")
+	}
+
 	var input = jsonmodel.ToProjectExportFromJSON(projectData)
 
 	alias := ""
diff --git a/server/internal/usecase/interactor/scene.go b/server/internal/usecase/interactor/scene.go
index 36668c4844..f97f772e5b 100644
--- a/server/internal/usecase/interactor/scene.go
+++ b/server/internal/usecase/interactor/scene.go
@@ -1,10 +1,8 @@
 package interactor
 
 import (
-	"archive/zip"
 	"context"
 	"errors"
-	"net/url"
 	"time"
 
 	"github.com/reearth/reearth/server/internal/usecase"
@@ -21,7 +19,6 @@ import (
 	"github.com/reearth/reearth/server/pkg/storytelling"
 	"github.com/reearth/reearth/server/pkg/visualizer"
 	"github.com/reearth/reearthx/idx"
-	"github.com/reearth/reearthx/log"
 	"github.com/reearth/reearthx/rerror"
 	"github.com/reearth/reearthx/usecasex"
 	"github.com/samber/lo"
@@ -647,7 +644,7 @@ func (i *Scene) RemoveCluster(ctx context.Context, sceneID id.SceneID, clusterID
 	return s, nil
 }
 
-func (i *Scene) ExportScene(ctx context.Context, prj *project.Project, zipWriter *zip.Writer) (*scene.Scene, map[string]interface{}, error) {
+func (i *Scene) ExportScene(ctx context.Context, prj *project.Project) (*scene.Scene, map[string]any, error) {
 
 	sce, err := i.sceneRepo.FindByProject(ctx, prj.ID())
 	if err != nil {
@@ -689,140 +686,14 @@ func (i *Scene) ExportScene(ctx context.Context, prj *project.Project, zipWriter
 	if err != nil {
 		return nil, nil, errors.New("Fail BuildResult :" + err.Error())
 	}
-	// plugin property
-	for _, v := range sceneJSON.Widgets {
-		for _, propInterface := range v.Property {
-			propSlice, ok := propInterface.([]interface{})
-			if !ok {
-				continue
-			}
-			for _, prop := range propSlice {
-				propMap, ok := prop.(map[string]interface{})
-				if !ok {
-					continue
-				}
-				if propType, exists := propMap["type"]; exists && propType == "url" {
-					if value, ok := propMap["value"].(string); ok {
-						if err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, value); err != nil {
-							log.Infofc(ctx, "Fail nLayer config addZipAsset :", err.Error())
-						}
-					}
-				}
-			}
-		}
-	}
-
-	// nlsLayer file resources
-	for _, nLayer := range nlsLayers {
-		if nLayer == nil {
-			continue
-		}
-		actualLayer := *nLayer
-		c := actualLayer.Config()
-		if c != nil {
-			actualConfig := *c
-			if data, ok := actualConfig["data"].(map[string]any); ok {
-				if urlStr, ok := data["url"].(string); ok {
-					if err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, urlStr); err != nil {
-						log.Infofc(ctx, "Fail nLayer config addZipAsset :", err.Error())
-					}
-				}
-			}
-		}
-
-		if actualLayer.Sketch() == nil ||
-			actualLayer.Sketch().FeatureCollection() == nil ||
-			actualLayer.Sketch().FeatureCollection().Features() == nil {
-			continue
-		}
-
-		for _, vFeature := range actualLayer.Sketch().FeatureCollection().Features() {
-			for _, value := range *vFeature.Properties() {
-				if v, ok := value.(string); ok {
-					if err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, v); err != nil {
-						log.Infofc(ctx, "Fail Sketch feature addZipAsset :", err.Error())
-					}
-				}
-
-			}
-		}
-	}
-	for _, lStyle := range *layerStyles {
-		if lStyle.Value() == nil {
-			continue
-		}
-		v := *lStyle.Value()
-		if marker, ok := v["marker"].(map[string]any); ok {
-			if image, ok := marker["image"].(string); ok {
-				if err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, image); err != nil {
-					log.Infofc(ctx, "Fail nLayer style addZipAsset :", err.Error())
-				}
-			}
-		}
-	}
-
-	var widgetPropertyIDs []idx.ID[id.Property]
-	for _, widget := range sce.Widgets().Widgets() {
-		widgetPropertyIDs = append(widgetPropertyIDs, widget.Property())
-	}
-	widgetProperties, err := i.propertyRepo.FindByIDs(ctx, widgetPropertyIDs)
-	if err != nil {
-		return nil, nil, errors.New("Fail widgetProperties :" + err.Error())
-	}
-
-	// widget button icon
-	for _, property := range widgetProperties {
-		for _, item := range property.Items() {
-			if item == nil {
-				continue
-			}
-			for _, field := range item.Fields(nil) {
-				if field == nil || field.Value() == nil || field.Value().Value() == nil {
-					continue
-				}
-				if field.GuessSchema().ID().String() == "buttonIcon" {
-					if u, ok := field.Value().Value().(*url.URL); ok {
-						if err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, u.Path); err != nil {
-							log.Infofc(ctx, "Fail widget addZipAsset :", err.Error())
-						}
-					}
-				}
-			}
-		}
-	}
-
-	var pagePropertyIDs []idx.ID[id.Property]
-	for _, page := range story.Pages().Pages() {
-		for _, block := range page.Blocks() {
-			pagePropertyIDs = append(pagePropertyIDs, block.Property())
-		}
-	}
-	pageProperties, err := i.propertyRepo.FindByIDs(ctx, pagePropertyIDs)
-	if err != nil {
-		return nil, nil, errors.New("Fail property :" + err.Error())
-	}
-	// page block src
-	for _, property := range pageProperties {
-		for _, item := range property.Items() {
-			for _, field := range item.Fields(nil) {
-				if field.GuessSchema().ID().String() == "src" {
-					if u, ok := field.Value().Value().(*url.URL); ok {
-						if err := AddZipAsset(ctx, i.assetRepo, i.file, zipWriter, u.Path); err != nil {
-							log.Infofc(ctx, "Fail widget addZipAsset :", err.Error())
-						}
-					}
-				}
-			}
-		}
-	}
 
-	res := make(map[string]interface{})
+	res := make(map[string]any)
 	res["scene"] = sceneJSON
 
 	return sce, res, nil
 }
 
-func (i *Scene) ImportScene(ctx context.Context, sce *scene.Scene, prj *project.Project, plgs []*plugin.Plugin, sceneData map[string]interface{}) (*scene.Scene, error) {
+func (i *Scene) ImportScene(ctx context.Context, sce *scene.Scene, prj *project.Project, plgs []*plugin.Plugin, sceneData map[string]any) (*scene.Scene, error) {
 	sceneJSON, err := builder.ParseSceneJSON(ctx, sceneData)
 	if err != nil {
 		return nil, err
diff --git a/server/internal/usecase/interfaces/asset.go b/server/internal/usecase/interfaces/asset.go
index a3316c27eb..86af3f9ade 100644
--- a/server/internal/usecase/interfaces/asset.go
+++ b/server/internal/usecase/interfaces/asset.go
@@ -4,14 +4,13 @@ import (
 	"archive/zip"
 	"context"
 	"errors"
-	"net/url"
 
 	"github.com/reearth/reearth/server/internal/usecase"
 	"github.com/reearth/reearth/server/pkg/asset"
 	"github.com/reearth/reearth/server/pkg/file"
 	"github.com/reearth/reearth/server/pkg/id"
+	"github.com/reearth/reearth/server/pkg/project"
 	"github.com/reearth/reearthx/account/accountdomain"
-	"github.com/reearth/reearthx/idx"
 	"github.com/reearth/reearthx/usecasex"
 )
 
@@ -40,5 +39,5 @@ type Asset interface {
 	Create(context.Context, CreateAssetParam, *usecase.Operator) (*asset.Asset, error)
 	Update(context.Context, id.AssetID, *id.ProjectID, *usecase.Operator) (id.AssetID, *id.ProjectID, error)
 	Remove(context.Context, id.AssetID, *usecase.Operator) (id.AssetID, error)
-	ImportAssetFiles(context.Context, string, *zip.File, idx.ID[accountdomain.Workspace], *id.ProjectID) (*url.URL, int64, error)
+	ImportAssetFiles(context.Context, map[string]*zip.File, []byte, *project.Project) ([]byte, error)
 }
diff --git a/server/internal/usecase/interfaces/plugin.go b/server/internal/usecase/interfaces/plugin.go
index 83152a9851..107aa5b60d 100644
--- a/server/internal/usecase/interfaces/plugin.go
+++ b/server/internal/usecase/interfaces/plugin.go
@@ -25,5 +25,5 @@ type Plugin interface {
 	UploadFromRemote(context.Context, *url.URL, id.SceneID, *usecase.Operator) (*plugin.Plugin, *scene.Scene, error)
 	ExportPlugins(context.Context, *scene.Scene, *zip.Writer) ([]*plugin.Plugin, []*property.Schema, error)
 	ImportPlugins(context.Context, *scene.Scene, []interface{}, []interface{}) ([]*plugin.Plugin, property.SchemaList, error)
-	ImporPluginFile(context.Context, string, string, *zip.File) error
+	ImporPluginFile(context.Context, map[string]*zip.File, string, string) error
 }
diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go
index d08c1e746c..77b285de40 100644
--- a/server/internal/usecase/interfaces/project.go
+++ b/server/internal/usecase/interfaces/project.go
@@ -72,6 +72,6 @@ type Project interface {
 	CheckAlias(context.Context, string) (bool, error)
 	Delete(context.Context, id.ProjectID, *usecase.Operator) error
 	ExportProjectData(context.Context, id.ProjectID, *zip.Writer, *usecase.Operator) (*project.Project, error)
-	ImportProjectData(context.Context, idx.ID[accountdomain.Workspace], map[string]interface{}, *usecase.Operator) (*project.Project, usecasex.Tx, error)
+	ImportProjectData(context.Context, idx.ID[accountdomain.Workspace], []byte, *usecase.Operator) (*project.Project, usecasex.Tx, error)
 	UploadExportProjectZip(context.Context, *zip.Writer, afero.File, map[string]interface{}, *project.Project) error
 }
diff --git a/server/internal/usecase/interfaces/scene.go b/server/internal/usecase/interfaces/scene.go
index 48b6b419ab..6a6f917ca2 100644
--- a/server/internal/usecase/interfaces/scene.go
+++ b/server/internal/usecase/interfaces/scene.go
@@ -1,7 +1,6 @@
 package interfaces
 
 import (
-	"archive/zip"
 	"context"
 	"errors"
 
@@ -33,8 +32,8 @@ type Scene interface {
 	AddCluster(context.Context, id.SceneID, string, *usecase.Operator) (*scene.Scene, *scene.Cluster, error)
 	UpdateCluster(context.Context, UpdateClusterParam, *usecase.Operator) (*scene.Scene, *scene.Cluster, error)
 	RemoveCluster(context.Context, id.SceneID, id.ClusterID, *usecase.Operator) (*scene.Scene, error)
-	ExportScene(context.Context, *project.Project, *zip.Writer) (*scene.Scene, map[string]interface{}, error)
-	ImportScene(context.Context, *scene.Scene, *project.Project, []*plugin.Plugin, map[string]interface{}) (*scene.Scene, error)
+	ExportScene(context.Context, *project.Project) (*scene.Scene, map[string]any, error)
+	ImportScene(context.Context, *scene.Scene, *project.Project, []*plugin.Plugin, map[string]any) (*scene.Scene, error)
 }
 
 type UpdateWidgetParam struct {


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

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

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

`42-42`: **Consider streaming or segmenting large imports to optimize memory usage.**

By receiving an entire asset payload in a `[]byte`, large imports can cause significant memory consumption. If you anticipate large inputs, consider a streaming or chunk-based approach to avoid performance bottlenecks and possible out-of-memory errors.

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

`28-28`: **Fix potential naming typo.**

The method name `ImporPluginFile` appears to be missing a "t." Consider renaming it for improved clarity:

```diff
- ImporPluginFile(context.Context, map[string]*zip.File, string, string) error
+ ImportPluginFile(context.Context, map[string]*zip.File, string, string) error
```

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

`647-695`: **Use descriptive error wrapping for better traceability.**  
Currently, errors are being created as `errors.New("Fail scene :" + err.Error())` (and similarly for other calls). This approach discards the original stack trace from the wrapped error. Consider using formatted error wrapping (e.g., `fmt.Errorf("scene retrieval failed: %w", err)`) to retain context and preserve the original error chain.



Below is an example diff showing how you might refactor one such line. Adapt as needed throughout this function:

```diff
- return nil, nil, errors.New("Fail scene :" + err.Error())
+ return nil, nil, fmt.Errorf("failed to retrieve scene: %w", err)
```

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

`228-230`: **Rename function to `ImportPluginFile`.**  
The function name `ImporPluginFile` misses a "t". Recommend correcting to maintain naming consistency.

---

`232-232`: **Handle ignored error.**  
`unmarshalPluginsScene` returns an error that is currently being discarded. For robustness, consider capturing or logging that error to aid debugging.

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

`530-553`: **Consider fallback logic for partial or relative URLs.**  
`strings.HasPrefix(v, adapter.CurrentHost(ctx))` might skip valid subdomains or alternative protocols. Consider using `url.Parse` or clarifying whether partial matches are out of scope.

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

`380-419`: **Typo in function name.**  
Similarly to the GQL import call, consider renaming `ImporPluginFile` → `ImportPluginFile`. This also helps maintain naming consistency across the codebase.

</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 61ed01025ae3eabe2320e3761a08f80887f57a49 and 467a4f541f332c7cd5b8cd9d5e284373d0ce78ec.

</details>

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

* `server/internal/adapter/gql/resolver_mutation_project.go` (5 hunks)
* `server/internal/usecase/interactor/asset.go` (3 hunks)
* `server/internal/usecase/interactor/common.go` (0 hunks)
* `server/internal/usecase/interactor/plugin.go` (10 hunks)
* `server/internal/usecase/interactor/project.go` (3 hunks)
* `server/internal/usecase/interactor/scene.go` (2 hunks)
* `server/internal/usecase/interfaces/asset.go` (2 hunks)
* `server/internal/usecase/interfaces/plugin.go` (1 hunks)
* `server/internal/usecase/interfaces/project.go` (1 hunks)
* `server/internal/usecase/interfaces/scene.go` (1 hunks)

</details>

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

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

</details>

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

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

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

```
Learnt from: hexaforce
PR: reearth/reearth-visualizer#1151
File: server/internal/adapter/gql/resolver_mutation_project.go:215-217
Timestamp: 2024-11-12T15:21:04.150Z
Learning: In the project import functionality, the file names are unique and can be safely replaced uniformly using `bytes.Replace`.
```

</details>

</details>

</details>

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

* GitHub Check: deploy-server / deploy_test
* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web

</details>

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

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

`75-75`: <details>
<summary>❓ Verification inconclusive</summary>

**Ensure usage has been updated for the new parameter type.**

Switching from `map[string]interface{}` to `[]byte` means all references to `ImportProjectData` must now supply raw bytes. Please confirm that callers have been updated to handle and structure this data properly. It may also be beneficial to document how these bytes are expected to be formed, validated, and parsed downstream.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Verify that all calls to ImportProjectData have been updated to pass raw []byte.

rg -A 5 'ImportProjectData'
```

Length of output: 1485

---



</details>

**Action: Confirm that all callers pass correctly formatted raw bytes and document the expected data structure**

The changes to the function’s signature in
- `server/internal/usecase/interfaces/project.go`
- `server/internal/usecase/interactor/project.go`
- `server/internal/adapter/gql/resolver_mutation_project.go`

show that the parameter type has been updated from `map[string]interface{}` to `[]byte`. The raw byte slice (`tempData`) is now being passed in all call sites. Please ensure that:

- All callers construct and pass the raw bytes as expected.
- There is clear documentation on how these bytes should be formatted, validated, and parsed downstream.

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

`35-35`: **Confirm signature consistency across the codebase.**  
The signature now uses `map[string]interface{}` whereas the implementing method uses `map[string]any`. Since `any` is an alias for `interface{}`, functional behavior should remain identical. However, consider standardizing the type alias across all references to keep the interface and implementation consistent.

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

`154-157`: **Looks good.**  
These lines correctly export the scene data after retrieving the project. The error handling is in place and looks adequate.

---

`168-171`: **Caution with possible nil data.**  
When `Normalize(data)` fails, it returns nil, which might cause downstream issues. Consider handling the case where the normalization fails instead of silently returning nil.

---

`200-203`: **Validate usage of `ImportProjectData`.**  
All callers of `ImportProjectData` should be updated accordingly to supply a byte slice instead of a map. If you haven't already, please confirm that no other references to the old signature remain.

---

`211-214`: **LGTM!**  
This asset import call conforms well to the new approach of rewriting references in `tempData`.

---

`216-219`: **Scene creation logic looks fine.**  
A new scene is created cleanly, with appropriate error checks in place.

---

`221-225`: **Old scene ID replacement is endorsed by prior learnings.**  
Using `bytes.Replace` is safe given the context that IDs are unique.

---

`235-235`: **Plugins import is handled properly.**  
Error detection is in place; on failure, the import process is canceled.

---

`243-245`: **Scene import logic looks fine.**  
The approach of passing in newly created references aligns well with the rest of the import flow.

---

`248-252`: **Style import logic seems correct.**  
No immediate issues noted.

---

`255-258`: **NLS layer import logic is consistent.**  
Checks for errors are properly handled.

---

`261-264`: **Story import flow is consistent with other imports.**  
Implementation seems coherent.

---

`282-293`: **Function design is clear.**  
It gracefully returns an error when the JSON doesn't contain a scene ID.

---

`295-306`: **The multi-value unmarshal approach is acceptable.**  
Type assertions might silently produce nils if the types are wrong, but error handling is present for the initial JSON parsing. Overall, this looks fine.

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

`556-591`: **Asset addition logic is sensible.**  
This function gracefully skips missing assets and captures known asset names into `assetNames`. The approach appears robust for the typical use case.

---

`595-611`: **Export includes asset metadata.**  
Assigning `data["assets"] = assetNames` is a straightforward way to embed collected asset references for the final JSON.

---

`642-681`: **Looks good.**  
Shifting from `map[string]interface{}` to raw JSON bytes is a clearer approach and allows consistent parsing steps within this method.

</details>
<details>
<summary>server/internal/usecase/interactor/plugin.go (3)</summary>

`293-293`: **Improved type usage.**  
Accepting slices of `any` instead of `[]interface{}` clarifies intent without changing functionality.

---

`331-342`: **Dynamic schema persistence is valid.**  
The quick type assertion to `map[string]any` is typical. Confirm that unexpected schema shapes won't cause deeper errors.

---

`367-378`: **Final retrieval of plugins and property schemas is straightforward.**  
This ensures the newly created records exist and are readable.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T23:30:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

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

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

`298-308`: **Improve error handling in type assertions.**

The type assertions in `unmarshalPluginsScene` don't check if the conversions are successful. If the JSON structure is not as expected, this could lead to nil values being passed to the import functions.


```diff
func unmarshalPluginsScene(data []byte) ([]any, map[string]any, []any, error) {
	var d map[string]any
	if err := json.Unmarshal(data, &d); err != nil {
		return nil, nil, nil, err
	}

-	pluginsData, _ := d["plugins"].([]any)
-	sceneData, _ := d["scene"].(map[string]any)
-	schemaData, _ := d["schemas"].([]any)
+	pluginsData, ok1 := d["plugins"].([]any)
+	if !ok1 && d["plugins"] != nil {
+		return nil, nil, nil, fmt.Errorf("plugins data is not in the expected format")
+	}
+	
+	sceneData, ok2 := d["scene"].(map[string]any)
+	if !ok2 && d["scene"] != nil {
+		return nil, nil, nil, fmt.Errorf("scene data is not in the expected format")
+	}
+	
+	schemaData, ok3 := d["schemas"].([]any)
+	if !ok3 && d["schemas"] != nil {
+		return nil, nil, nil, fmt.Errorf("schema data is not in the expected format")
+	}
	return pluginsData, sceneData, schemaData, nil
}
```

---

`285-296`: **Add improved error message in getOldSceneID.**

The error message "scene id not found" could be more descriptive to help with debugging.


```diff
func getOldSceneID(data []byte) (string, error) {
	var d map[string]any
	if err := json.Unmarshal(data, &d); err != nil {
-		return "", err
+		return "", fmt.Errorf("failed to unmarshal scene data: %w", err)
	}
	if s, ok := d["scene"].(map[string]any); ok {
		if id, ok := s["id"].(string); ok {
			return id, nil
		}
+		return "", errors.New("scene id is not a string or not present in scene data")
	}
-	return "", errors.New("scene id not found")
+	return "", errors.New("scene object not found in project data")
}
```

</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 467a4f541f332c7cd5b8cd9d5e284373d0ce78ec and ee581be1c831b7bf5f8c7de843bb8f34a488f9ed.

</details>

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

* `server/internal/adapter/gql/resolver_mutation_project.go` (5 hunks)

</details>

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

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

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

```
Learnt from: hexaforce
PR: reearth/reearth-visualizer#1151
File: server/internal/adapter/gql/resolver_mutation_project.go:215-217
Timestamp: 2024-11-12T15:21:04.150Z
Learning: In the project import functionality, the file names are unique and can be safely replaced uniformly using `bytes.Replace`.
```

```
Learnt from: hexaforce
PR: reearth/reearth-visualizer#1151
File: server/internal/adapter/gql/resolver_mutation_project.go:0-0
Timestamp: 2024-11-12T15:21:04.150Z
Learning: In the project export functionality, the ZIP file is deleted after saving to storage.
```

</details>

</details>

</details>

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

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

`178-186`: **Return errors from `Normalize` instead of nil.**

Currently, any JSON marshalling/unmarshalling error silently yields `nil`. This may hide parsing issues and lead to broken exports. Consider returning an error to help callers differentiate legitimate empty data from serialization failures.


```diff
-func Normalize(data any) map[string]any {
-	if b, err := json.Marshal(data); err == nil {
-		var result map[string]any
-		if err := json.Unmarshal(b, &result); err == nil {
-			return result
-		}
-	}
-	return nil
+func Normalize(data any) (map[string]any, error) {
+	b, err := json.Marshal(data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal: %w", err)
+	}
+	var result map[string]any
+	if err := json.Unmarshal(b, &result); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal: %w", err)
+	}
+	return result, nil
}
```

---

`216-219`: **LGTM: Scene creation immediately after project import.**

Creating a new scene immediately after importing project data is a good approach, as it simplifies the overall flow and makes the scene available for subsequent operations.

---

`248-248`: **Improved error messages with "sceneJSON" prefix.**

Prefixing error messages with "sceneJSON" provides clearer context about which part of the import process failed, making debugging easier.

---

`225-225`: <details>
<summary>❓ Verification inconclusive</summary>

**Validate the usage of bytes.Replace for scene ID substitution.**

The usage of `bytes.Replace` aligns with the project's practices (as noted in retrieved learnings), but ensure all references to the scene ID are properly replaced, including in nested structures.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any potential areas where scene IDs might be nested and missed by the simple replace
cat server/internal/adapter/gql/resolver_mutation_project.go | grep -C 2 "bytes.Replace"

# Look for other places in the codebase where scene IDs are handled to verify consistency
rg "scene.*id.*replace" --type go
```

Length of output: 2334

---



</details>

**Action: Confirm comprehensive scene ID replacement.**

- The usage of `bytes.Replace` in the resolver (server/internal/adapter/gql/resolver_mutation_project.go) is consistent with our project’s approach.
- However, note that scene IDs might be embedded in nested structures (e.g., within JSON payloads processed by functions like `parseWidgetZone` or `setWidgetSection` in other modules). Please verify that these nested references are either handled elsewhere or do not require similar substitution.
- A review of the related functions in files such as server/pkg/scene/builder/decoder.go and server/internal/usecase/interactor/* is advised to ensure full coverage.

---

`232-235`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider handling nil values from unmarshalPluginsScene.**

The function currently doesn't handle the case where `unmarshalPluginsScene` might return nil values for pluginsData, sceneJsonData, or schemaData, which could lead to downstream errors.


Verify how the downstream functions handle nil values:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if ImportPlugins, ImportScene etc. properly handle nil inputs
rg -A 5 "ImportPlugins.*ctx.*nil" --type go
rg -A 5 "ImportScene.*ctx.*nil" --type go
rg -A 5 "ImportStyles.*ctx.*nil" --type go
rg -A 5 "ImportNLSLayers.*ctx.*nil" --type go
rg -A 5 "ImportStory.*ctx.*nil" --type go
```

Length of output: 715

---



</details>

**Action Required: Address Possible Nil Handling Issues in `unmarshalPluginsScene` Output**

The current implementation in `server/internal/adapter/gql/resolver_mutation_project.go` (lines 232–235) does not verify that the outputs from `unmarshalPluginsScene` (i.e. `pluginsData`, `sceneJsonData`, and `schemaData`) are non-nil before proceeding. Although our search found that one test (for `ImportNLSLayers`) passes `nil` explicitly and expects non-nil results downstream, there’s limited evidence that all downstream functions (such as those for plugins, scene, styles, and story) are robust against nil inputs.

**Recommendations:**
- Add explicit nil checks for each of the outputs from `unmarshalPluginsScene`. For example, if any returned value is nil despite no error, return an appropriate error before calling downstream functions.
- Ensure consistency across downstream functions if nil inputs can lead to panics or unexpected behavior.
- Verify that all downstream consumers (e.g. functions like `ImportPlugins`, `ImportScene`, `ImportStyles`, and `ImportStory`) can safely handle nil values or adjust their usage accordingly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T23:34:27Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (4)</summary><blockquote>

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

`156-226`: _🛠️ Refactor suggestion_

**Improve asset file loading and closure to prevent resource leaks.**

The current implementation defers closing file resources until the end of the method, which could lead to too many open files with large imports.



Apply this change to read file content immediately and close readers promptly:

```diff
readCloser, err := zipFile.Open()
if err != nil {
    return nil, err
}
-defer func() {
-    if cerr := readCloser.Close(); cerr != nil {
-        fmt.Printf("Error closing file: %v\n", cerr)
-    }
-}()

+content, err := io.ReadAll(readCloser)
+if cerr := readCloser.Close(); cerr != nil {
+    fmt.Printf("Error closing file: %v\n", cerr)
+}
+if err != nil {
+    return nil, err
+}

file := &file.File{
-    Content:     readCloser,
+    Content:     bytes.NewReader(content),
    Path:        realName,
    Size:        int64(zipFile.UncompressedSize64),
    ContentType: http.DetectContentType([]byte(zipFile.Name)),
}
```

---

`164-168`: _⚠️ Potential issue_

**Add validation for asset mapping to prevent potential panic.**

The code directly casts `d["assets"]` to `map[string]any` without validating its existence or type, which could cause a panic if the "assets" key is missing or not a map.



Apply this change to add proper validation:

```diff
-assetNames := make(map[string]string)
-for beforeName, realName := range d["assets"].(map[string]any) {
-    if realName, ok := realName.(string); ok {
-        assetNames[beforeName] = realName
-    }
-}
+assetNames := make(map[string]string)
+assetsMap, ok := d["assets"].(map[string]any)
+if !ok {
+    return nil, errors.New("missing or invalid \"assets\" field in JSON data")
+}
+for beforeName, realName := range assetsMap {
+    if realName, ok := realName.(string); ok {
+        assetNames[beforeName] = realName
+    }
+}
```

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

`186-194`: _🛠️ Refactor suggestion_

**Improve the Normalize function to handle errors properly.**

Currently, any JSON marshalling/unmarshalling error silently yields `nil`, which may hide parsing issues and lead to broken exports.



Apply this change to properly handle and report errors:

```diff
-func Normalize(data any) map[string]any {
-    if b, err := json.Marshal(data); err == nil {
-        var result map[string]any
-        if err := json.Unmarshal(b, &result); err == nil {
-            return result
-        }
-    }
-    return nil
+func Normalize(data any) (map[string]any, error) {
+    b, err := json.Marshal(data)
+    if err != nil {
+        return nil, fmt.Errorf("failed to marshal: %w", err)
+    }
+    var result map[string]any
+    if err := json.Unmarshal(b, &result); err != nil {
+        return nil, fmt.Errorf("failed to unmarshal: %w", err)
+    }
+    return result, nil
}
```

---

`176-176`: _⚠️ Potential issue_

**Handle potential nil return from Normalize function.**

Since `Normalize` may return nil if marshalling/unmarshalling fails, error handling should be added to prevent potential nil pointer dereferences.



Once the `Normalize` function is updated to return errors, this code should be updated to:

```diff
-err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, Normalize(data), prj)
+normalizedData, err := Normalize(data)
+if err != nil {
+    return nil, fmt.Errorf("failed to normalize project data: %w", err)
+}
+err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, normalizedData, prj)
```

</blockquote></details>

</blockquote></details>

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

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

`529-553`: **Consider handling large or cyclic data structures**  
`SearchAssetURL` recurses through nested maps/arrays. A malicious or deeply nested structure might cause excessive recursion or a potential stack overflow. Consider implementing a maximum depth or a visited set to prevent infinite loops.

---

`595-610`: **Avoid overshadowing the imported “path” package**  
Inside this block, a local variable named `path` is used, which also references the imported package `path`. Although scoped, this can be confusing. You may want to use a different variable name, such as `assetPath`.  

```diff
- if path, ok := imageUrl["Path"].(string); ok {
+ if assetPath, ok := imageUrl["Path"].(string); ok {
    ...
}
```

---

`649-657`: **Gracefully handle missing "project" key**  
Relying on a "project" map key can raise user-facing errors for unexpected JSON. Ensure this is a conscious choice or consider more user-friendly error messages.

</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 ee581be1c831b7bf5f8c7de843bb8f34a488f9ed and b575c8cb358e5f6ddf639e8133c759b1bab92e4b.

</details>

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

* `server/internal/adapter/gql/resolver_mutation_project.go` (6 hunks)
* `server/internal/usecase/interactor/asset.go` (3 hunks)
* `server/internal/usecase/interactor/common.go` (0 hunks)
* `server/internal/usecase/interactor/plugin.go` (10 hunks)
* `server/internal/usecase/interactor/project.go` (3 hunks)
* `server/internal/usecase/interactor/scene.go` (2 hunks)
* `server/internal/usecase/interfaces/asset.go` (2 hunks)
* `server/internal/usecase/interfaces/plugin.go` (1 hunks)
* `server/internal/usecase/interfaces/project.go` (1 hunks)
* `server/internal/usecase/interfaces/scene.go` (1 hunks)

</details>

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

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

</details>

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

* server/internal/usecase/interfaces/plugin.go
* server/internal/usecase/interfaces/project.go
* server/internal/usecase/interfaces/scene.go
* server/internal/usecase/interactor/plugin.go

</details>

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

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

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

```
Learnt from: hexaforce
PR: reearth/reearth-visualizer#1151
File: server/internal/adapter/gql/resolver_mutation_project.go:215-217
Timestamp: 2024-11-12T15:21:04.150Z
Learning: In the project import functionality, the file names are unique and can be safely replaced uniformly using `bytes.Replace`.
```

</details>

</details>

</details>

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

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

`42-42`: **Method signature updated to handle multiple assets.**

The `ImportAssetFiles` method now accepts a map of assets and returns processed data bytes, which is more flexible than the previous single file approach. This change aligns with the project import refactoring to handle multiple assets more efficiently.

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

`647-694`: **ZIP handling logic removed from ExportScene method.**

The method no longer takes a `zipWriter` parameter and has been simplified to just return scene data. This is a good architectural change as it separates concerns - the Scene interactor now focuses on data preparation without file packaging responsibilities.

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

`10-12`: **No concerns with the new imports**  
They are used appropriately for URL and path manipulations.

---

`16-16`: **Import update looks good**  
No issues found with referencing this new adapter import.

---

`526-528`: **Verify removal of image handling logic**  
Previously, `ExportProjectData` handled or returned image data. Now the function only returns `prj`. Please ensure no other code paths rely on the older image export logic.

---

`555-591`: **Avoid silently ignoring missing assets**  
When `file.ReadAsset` fails, the code currently returns `nil`. This hides potential issues if the asset truly should exist. Consider logging or returning an error for better transparency and troubleshooting.

---

`616-620`: **Indented JSON is a welcome addition**  
Using `json.MarshalIndent` improves the readability of the exported JSON.

---

`642-682`: **Validate size and structure of imported data**  
`ImportProjectData` now accepts potentially large byte slices, which are unmarshaled directly. Consider validating input size or adding further checks to guard against malformed JSON or potential memory exhaustion.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T00:12:06Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

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

`164-164`: _⚠️ Potential issue_

**Unsafe type assertion could cause panic**

The code directly casts `d["assets"]` to `map[string]any` without first checking if it exists or has the right type. This could cause a runtime panic if the "assets" key is missing or has a different structure.



Apply this fix:
```diff
- for beforeName, realName := range d["assets"].(map[string]any) {
+ assetsMap, ok := d["assets"].(map[string]any)
+ if !ok {
+   return nil, fmt.Errorf("missing or invalid \"assets\" field in JSON data")
+ }
+ for beforeName, realName := range assetsMap {
```

---

`170-226`: _🛠️ Refactor suggestion_

**Inefficient file handling in loop**

Deferring file closure in a loop can lead to many files remaining open until the function returns. For large imports, this could exhaust file descriptors. Instead, read the file content and close it immediately.



```diff
  readCloser, err := zipFile.Open()
  if err != nil {
    return nil, err
  }
- defer func() {
-   if cerr := readCloser.Close(); cerr != nil {
-     fmt.Printf("Error closing file: %v\n", cerr)
-   }
- }()
+ fileContent, err := io.ReadAll(readCloser)
+ if err != nil {
+   readCloser.Close() // Close immediately on error
+   return nil, err
+ }
+ if err := readCloser.Close(); err != nil {
+   fmt.Printf("Error closing file: %v\n", err)
+ }

  file := &file.File{
-   Content:     readCloser,
+   Content:     bytes.NewReader(fileContent),
    Path:        realName,
    Size:        int64(zipFile.UncompressedSize64),
    ContentType: http.DetectContentType([]byte(zipFile.Name)),
  }
```

</blockquote></details>

</blockquote></details>

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

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

`225-225`: **Inefficient string replacement**

Using `bytes.Replace` replaces only the first occurrence by default. For more comprehensive replacement, consider using `bytes.ReplaceAll` introduced in Go 1.12. Additionally, this approach could be inefficient for large data.


```diff
- data = bytes.Replace(data, []byte(beforeName), []byte(afterName), -1)
+ data = bytes.ReplaceAll(data, []byte(beforeName), []byte(afterName))
```

</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 b575c8cb358e5f6ddf639e8133c759b1bab92e4b and 89a6582a73fecddccf5ff79a13e3022c98c49882.

</details>

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

* `server/internal/usecase/interactor/asset.go` (3 hunks)

</details>

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

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

`198-206`: <details>
<summary>✅ Verification successful</summary>

**Add missing import**

The `path` package is being used, but there's no specific check if the function is being used correctly. Make sure `path.Base` is appropriate for URL paths - `filepath.Base` might be more suitable depending on the platform.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the URLs in the project are properly constructed
# and if path.Base is appropriate

# Look for URL usages in the project
rg -A 3 "ImageURL\\(\\)" --type go
```

Length of output: 1922

---



</details>

**Ensure Correct URL Base Extraction**

The verification confirms that using `path.Base(newProject.ImageURL().Path)` is appropriate for URL paths, as URLs always use forward slashes. There’s no need to change to `filepath.Base` unless you intend to work with OS-specific file paths. Please verify that the `"path"` package is correctly imported in the file.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T00:30:05Z
user: login: soneda-yuya
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T00:53:25Z

Review comments:
diff_hunk: @@ -151,45 +153,74 @@ func (i *Asset) Remove(ctx context.Context, aid id.AssetID, operator *usecase.Op
 	)
 }
 
-func (i *Asset) ImportAssetFiles(ctx context.Context, realName string, zipFile *zip.File, workspace idx.ID[accountdomain.Workspace], project *id.ProjectID) (*url.URL, int64, error) {
+func (i *Asset) ImportAssetFiles(ctx context.Context, assets map[string]*zip.File, data []byte, newProject *project.Project) ([]byte, error) {
 
-	readCloser, err := zipFile.Open()
-	if err != nil {
-		return nil, 0, fmt.Errorf("error opening zip file entry: %w", err)
+	var d map[string]any
+	if err := json.Unmarshal(data, &d); err != nil {
+		return nil, err
 	}
-	defer func() {
-		if cerr := readCloser.Close(); cerr != nil {
-			fmt.Printf("Error closing file: %v\n", cerr)
+
+	assetNames := make(map[string]string)
+	for beforeName, realName := range d["assets"].(map[string]any) {
+		if realName, ok := realName.(string); ok {
+			assetNames[beforeName] = realName
 		}
-	}()
-
-	contentType := http.DetectContentType([]byte(zipFile.Name))
-	file := &file.File{
-		Content:     readCloser,
-		Path:        realName,
-		Size:        int64(zipFile.UncompressedSize64),
-		ContentType: contentType,
-	}
-	url, size, err := i.gateways.File.UploadAsset(ctx, file)
-	if err != nil {
-		return nil, 0, err
 	}
 
-	a, err := asset.New().
-		NewID().
-		Workspace(workspace).
-		Project(project).
-		Name(path.Base(realName)).
-		Size(size).
-		URL(url.String()).
-		CoreSupport(true).
-		Build()
-	if err != nil {
-		return nil, 0, err
-	}
-	if err := i.repos.Asset.Save(ctx, a); err != nil {
-		return nil, 0, err
+	for beforeName, zipFile := range assets {
+		realName := assetNames[beforeName]
+		readCloser, err := zipFile.Open()
+		if err != nil {
+			return nil, err
+		}
+
+		defer func() {
+			if cerr := readCloser.Close(); cerr != nil {
+				fmt.Printf("Error closing file: %v\n", cerr)
+			}
+		}()
+
+		file := &file.File{
+			Content:     readCloser,
+			Path:        realName,
+			Size:        int64(zipFile.UncompressedSize64),
+			ContentType: http.DetectContentType([]byte(zipFile.Name)),
+		}
+
+		url, size, err := i.gateways.File.UploadAsset(ctx, file)
+		if err != nil {
+			return nil, err
+		}
+
+		if newProject.ImageURL() != nil {
+			if path.Base(newProject.ImageURL().Path) == beforeName {
+				newProject.SetImageURL(url)
+				err := i.repos.Project.Save(ctx, newProject)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+
+		a, err := asset.New().
+			NewID().
+			Workspace(newProject.Workspace()).
+			Project(newProject.ID().Ref()).
+			Name(path.Base(realName)).
+			Size(size).
+			URL(url.String()).
+			CoreSupport(true).
+			Build()
+		if err != nil {
+			return nil, err
+		}
+
+		if err := i.repos.Asset.Save(ctx, a); err != nil {
+			return nil, err
+		}
+		afterName := path.Base(url.Path)
+		data = bytes.Replace(data, []byte(beforeName), []byte(afterName), -1)
 	}
 
-	return url, size, err
+	return data, nil
path: server/internal/usecase/interactor/asset.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Avoid potential panic and manage file closures more efficiently.**  

1. *Check the presence of the "assets" key:*  
   The code casts `d["assets"]` to `map[string]any` without validating its existence or type. If `assets` is missing or has a different structure, this will panic. Consider verifying it first:
   ```diff
   + assetsMap, ok := d["assets"].(map[string]any)
   + if !ok {
   +   return nil, errors.New("missing or invalid \"assets\" field in JSON data")
   + }
   ```

2. *Close files promptly:*  
   Deferring `readCloser.Close()` in each iteration means many files could remain open until the function returns, potentially exhausting file descriptors in large imports. Instead, read the file content immediately and close the reader right away:
   ```diff
   readCloser, err := zipFile.Open()
   if err != nil {
     return nil, err
   }
   - defer readCloser.Close()
   + content, err := io.ReadAll(readCloser)
   + _ = readCloser.Close() // Close ASAP
   if err != nil {
     return nil, err
   }
   // use "content" now, e.g. in file.File
   ```




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

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T23:30:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -203,6 +175,16 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp
 	}, nil
 }
 
+func Normalize(data any) map[string]any {
+	if b, err := json.Marshal(data); err == nil {
+		var result map[string]any
+		if err := json.Unmarshal(b, &result); err == nil {
+			return result
+		}
+	}
+	return nil
+}
path: server/internal/adapter/gql/resolver_mutation_project.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Return errors from `Normalize` instead of nil.**  
Currently, any JSON marshalling/unmarshalling error silently yields `nil`. This may hide parsing issues and lead to broken exports. Consider returning an error to help callers differentiate legitimate empty data from serialization failures.  


```diff
-func Normalize(data any) map[string]any {
-	if b, err := json.Marshal(data); err == nil {
-		var result map[string]any
-		if err := json.Unmarshal(b, &result); err == nil {
-			return result
-		}
-	}
-	return nil
+func Normalize(data any) (map[string]any, error) {
+	b, err := json.Marshal(data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal: %w", err)
+	}
+	var result map[string]any
+	if err := json.Unmarshal(b, &result); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal: %w", err)
+	}
+	return result, nil
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
func Normalize(data any) (map[string]any, error) {
	b, err := json.Marshal(data)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal: %w", err)
	}
	var result map[string]any
	if err := json.Unmarshal(b, &result); err != nil {
		return nil, fmt.Errorf("failed to unmarshal: %w", err)
	}
	return result, nil
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T23:30:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,34 +161,11 @@ func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.Exp
 		return nil, errors.New("Fail ExportPlugins :" + err.Error())
 	}
 
-	page := usecasex.CursorPagination{
-		First: lo.ToPtr(int64(-1)),
-	}.Wrap()
-	sort := &asset.SortType{
-		Key:  "id",
-		Desc: true,
-	}
-
-	assets, _, err := usecases(ctx).Asset.FindByWorkspaceProject(ctx, prj.Workspace(), nil, nil, sort, page, getOperator(ctx))
-	if err != nil {
-		return nil, errors.New("Fail ExportAsset :" + err.Error())
-	}
-	assetNames := make(map[string]string)
-	for _, a := range gqlmodel.ToAssets(assets) {
-		parsedURL, err := url.Parse(a.URL)
-		if err != nil {
-			return nil, errors.New("Error parsing URL:" + err.Error())
-		}
-		fileName := path.Base(parsedURL.Path)
-		assetNames[fileName] = a.Name
-	}
-
 	data["project"] = gqlmodel.ToProjectExport(prj)
 	data["plugins"] = gqlmodel.ToPlugins(plgs)
 	data["schemas"] = gqlmodel.ToPropertySchemas(schemas)
-	data["assets"] = assetNames
 
-	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, data, prj)
+	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, Normalize(data), prj)
path: server/internal/adapter/gql/resolver_mutation_project.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle potential nil return from `Normalize` function.**

The `Normalize` function may return nil if marshalling or unmarshalling fails, but the current code doesn't check for this possibility. This could lead to unexpected behavior if the data can't be normalized properly.


```diff
-	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, Normalize(data), prj)
+	normalizedData := Normalize(data)
+	if normalizedData == nil {
+		return nil, errors.New("Failed to normalize project data")
+	}
+	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, normalizedData, prj)
```

Once the `Normalize` function is updated to return errors as suggested in the previous comment, update this code to:

```diff
-	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, Normalize(data), prj)
+	normalizedData, err := Normalize(data)
+	if err != nil {
+		return nil, errors.New("Failed to normalize project data: " + err.Error())
+	}
+	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, normalizedData, prj)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	normalizedData, err := Normalize(data)
	if err != nil {
		return nil, errors.New("Failed to normalize project data: " + err.Error())
	}
	err = usecases(ctx).Project.UploadExportProjectZip(ctx, zipWriter, zipFile, normalizedData, prj)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T23:34:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,45 +153,77 @@ func (i *Asset) Remove(ctx context.Context, aid id.AssetID, operator *usecase.Op
 	)
 }
 
-func (i *Asset) ImportAssetFiles(ctx context.Context, realName string, zipFile *zip.File, workspace idx.ID[accountdomain.Workspace], project *id.ProjectID) (*url.URL, int64, error) {
+func (i *Asset) ImportAssetFiles(ctx context.Context, assets map[string]*zip.File, data []byte, newProject *project.Project) ([]byte, error) {
 
-	readCloser, err := zipFile.Open()
-	if err != nil {
-		return nil, 0, fmt.Errorf("error opening zip file entry: %w", err)
+	var d map[string]any
+	if err := json.Unmarshal(data, &d); err != nil {
+		return nil, err
 	}
-	defer func() {
-		if cerr := readCloser.Close(); cerr != nil {
-			fmt.Printf("Error closing file: %v\n", cerr)
+
+	assetNames := make(map[string]string)
+	for beforeName, realName := range d["assets"].(map[string]any) {
+		if realName, ok := realName.(string); ok {
+			assetNames[beforeName] = realName
 		}
-	}()
-
-	contentType := http.DetectContentType([]byte(zipFile.Name))
-	file := &file.File{
-		Content:     readCloser,
-		Path:        realName,
-		Size:        int64(zipFile.UncompressedSize64),
-		ContentType: contentType,
-	}
-	url, size, err := i.gateways.File.UploadAsset(ctx, file)
-	if err != nil {
-		return nil, 0, err
 	}
 
-	a, err := asset.New().
-		NewID().
-		Workspace(workspace).
-		Project(project).
-		Name(path.Base(realName)).
-		Size(size).
-		URL(url.String()).
-		CoreSupport(true).
-		Build()
-	if err != nil {
-		return nil, 0, err
-	}
-	if err := i.repos.Asset.Save(ctx, a); err != nil {
-		return nil, 0, err
+	for beforeName, zipFile := range assets {
+		if zipFile.UncompressedSize64 == 0 {
+			continue
+		}
+		realName := assetNames[beforeName]
+		readCloser, err := zipFile.Open()
+		if err != nil {
+			return nil, err
+		}
+
+		defer func() {
+			if cerr := readCloser.Close(); cerr != nil {
+				fmt.Printf("Error closing file: %v\n", cerr)
+			}
+		}()
+
+		file := &file.File{
+			Content:     readCloser,
+			Path:        realName,
+			Size:        int64(zipFile.UncompressedSize64),
+			ContentType: http.DetectContentType([]byte(zipFile.Name)),
path: server/internal/usecase/interactor/asset.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Incorrect content type detection**

Using the file name for content type detection is incorrect. Content type should be determined from the actual file content, not the name.


```diff
- ContentType: http.DetectContentType([]byte(zipFile.Name)),
+ ContentType: http.DetectContentType(fileContent[:min(512, len(fileContent))]),
```

You'll need to add this helper function:
```go
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T00:30:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-06T23:23:43Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The pull request refactors the export and import logic for projects, scenes, assets, and plugins across both adapter and usecase layers. Changes include the removal of outdated asset handling such as zip packaging and image path processing, and significant updates to function signatures—transitioning from `interface{}` to `any` and switching from single file parameters to maps for multi-file processing. New functions like `Normalize` and asset URL search utilities are introduced, while redundant code for ZIP asset handling has been removed.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `server/internal/adapter/gql/resolver_mutation_project.go` | Refactored `ExportProject` and `ImportProject`: removed asset handling, eliminated `zipWriter` parameter, introduced `Normalize`, and streamlined error messages. |
| `server/internal/usecase/interactor/asset.go` <br> `server/internal/usecase/interfaces/asset.go` | Updated `ImportAssetFiles` to accept a map of zip files and JSON data; revised logic to process multiple assets with improved error handling. |
| `server/internal/usecase/interactor/plugin.go` <br> `server/internal/usecase/interfaces/plugin.go` | Modified plugin functions: updated signatures to use `any` type; `ImporPluginFile` now accepts a map of zip files; enhanced schema parsing logic. |
| `server/internal/usecase/interactor/common.go` | Removed the `AddZipAsset` function along with related ZIP file handling and imports. |
| `server/internal/usecase/interactor/project.go` <br> `server/internal/usecase/interfaces/project.go` | Introduced `SearchAssetURL` and a new `AddZipAsset` function; adjusted `ExportProjectData` and `ImportProjectData` to simplify asset and image handling, switching from map to byte slice input. |
| `server/internal/usecase/interactor/scene.go` <br> `server/internal/usecase/interfaces/scene.go` | Updated `ExportScene` by removing the `zip.Writer` parameter and changing the return type to use `any` instead of `interface{}`, simplifying scene export logic. |

## Possibly related PRs

- **reearth/reearth-visualizer#1474**: Direct connection in asset handling changes, particularly in how assets are processed during project export and import.
- **reearth/reearth-visualizer#1239**: Modifications in the `ExportProject` and `ImportProject` methods align closely with the adjustments in this PR.
- **reearth/reearth-visualizer#1151**: Enhancements to ZIP file handling in the export/import processes establish a strong code-level link with these changes.

## Suggested labels

`web`

## Suggested reviewers

- pyshx
- soneda-yuya
- mkumbobeaty

## Poem

> I’m a code rabbit, hopping along the stream,  
> Simplifying logic like a mid-day dream.  
> Assets now dance in a streamlined groove,  
> Functions updated, each in its smooth move.  
> Happy hops to refactored code — up, up, and away!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_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=1477):

- 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-03-06T23:23:53Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *reearth-web* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 72acd1b084c4adcebd7b495f18c70e3ec2e2172a |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/reearth-web/deploys/67ca439e51d3770008042b52 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1477--reearth-web.netlify.app](https://deploy-preview-1477--reearth-web.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE0NzctLXJlZWFydGgtd2ViLm5ldGxpZnkuYXBwIn0.noxUN0udRYUnsqiUrU7kBrCYaU0EziJKp61ss6ZGIuo)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [reearth-web Preview](https://deploy-preview-1477--reearth-web.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/reearth-web/configuration/notifications#deploy-webhooks)._
author_association: NONE