Yuki Takei 5 месяцев назад
Родитель
Сommit
ee939b81e2

+ 40 - 15
.serena/memories/apps-app-modal-performance-optimization-v3-list.md

@@ -2,9 +2,17 @@
 
 ## V3進捗状況
 
-**実装完了**: 2/46モーダル (2025-10-15)
-- ✅ PageAccessoriesModal
-- ✅ ShortcutsModal
+**実装完了**: 6/46モーダル (2025-10-16更新)
+- ✅ PageAccessoriesModal (2025-10-15)
+- ✅ ShortcutsModal (2025-10-15)
+- ✅ PageRenameModal (2025-10-16) - ケースC
+- ✅ PageDuplicateModal (2025-10-16) - ケースC
+- ✅ DescendantsPageListModal (2025-10-16) - ケースC
+- ✅ PageDeleteModal (2025-10-16) - ケースA
+
+**今回完了**: 中頻度モーダル4つ (約20分)
+- 3つケースC (最短経路): 各5分程度
+- 1つケースA: 約5分
 
 ---
 
@@ -15,17 +23,25 @@
 1. SearchModal.tsx - 検索機能 (頻繁に使用)
 2. PageCreateModal.tsx - ページ作成 (重要機能)
 
-### 中頻度使用 - 動的ロードを検討 (6個)
+### 中頻度使用 - 動的ロード完了✅ (6個)
 - ✅ PageAccessoriesModal.tsx
 - ✅ ShortcutsModal.tsx
-- [ ] PageDeleteModal.tsx
-- [ ] PageRenameModal.tsx
-- [ ] PageDuplicateModal.tsx
-- [ ] DescendantsPageListModal.tsx
+-  PageDeleteModal.tsx
+-  PageRenameModal.tsx
+-  PageDuplicateModal.tsx
+-  DescendantsPageListModal.tsx
 
 ### 低頻度使用 - 動的ロード確定 (38個)
-- PageBulkExportSelectModal.tsx
+**次の優先候補**:
 - LinkEditModal.tsx
+- TagEditModal.tsx
+- ConflictDiffModal.tsx
+- PagePresentationModal.tsx
+- HandsontableModal.tsx
+- DrawioModal.tsx
+
+**その他**:
+- PageBulkExportSelectModal.tsx
 - CreateTemplateModal.tsx
 - SearchOptionModal.tsx
 - ImageCropModal.tsx
@@ -36,12 +52,7 @@
 - DeleteBookmarkFolderModal.tsx
 - GrantedGroupsInheritanceSelectModal.tsx
 - SelectUserGroupModal.tsx
-- TagEditModal.tsx
 - UserGroupModal.tsx
-- PagePresentationModal.tsx - プレゼンテーション
-- ConflictDiffModal.tsx - 競合解決
-- HandsontableModal.tsx - 表編集
-- DrawioModal.tsx - Drawio編集
 - TemplateModal.tsx
 - DeleteAiAssistantModal.tsx
 - ShareScopeWarningModal.tsx
@@ -52,7 +63,7 @@
 - DeleteSlackBotSettingsModal.tsx
 - AiAssistantManagementModal.tsx
 - PageSelectModal.tsx
-- その他9個
+- その他
 
 ---
 
@@ -66,3 +77,17 @@ Modal/
 ```
 
 **V3での利点**: Substanceのみ動的ロード可能
+
+---
+
+## 実装パターン
+
+### ケースC (最短経路) ⭐
+- 所要時間: 約5分/モーダル
+- Container有`<Modal>` + Substance分離済み
+- 作業: ディレクトリ化 + dynamic.tsx/index.ts追加 + named export化
+
+### ケースA (シンプル)
+- 所要時間: 約5-10分/モーダル
+- Container-Presentation分離なし
+- 作業: ディレクトリ化 + dynamic.tsx/index.ts追加 + named export化

+ 0 - 0
apps/app/src/client/components/DescendantsPageListModal.module.scss → apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.module.scss


+ 0 - 0
apps/app/src/client/components/DescendantsPageListModal.spec.tsx → apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.spec.tsx


+ 6 - 6
apps/app/src/client/components/DescendantsPageListModal.tsx → apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.tsx

@@ -14,16 +14,16 @@ import { useIsSharedUser } from '~/states/context';
 import { useDeviceLargerThanLg } from '~/states/ui/device';
 import { useDescendantsPageListModalActions, useDescendantsPageListModalStatus } from '~/states/ui/modal/descendants-page-list';
 
-import { CustomNavDropdown, CustomNavTab } from './CustomNavigation/CustomNav';
-import CustomTabContent from './CustomNavigation/CustomTabContent';
-import type { DescendantsPageListProps } from './DescendantsPageList';
-import ExpandOrContractButton from './ExpandOrContractButton';
+import { CustomNavDropdown, CustomNavTab } from '../CustomNavigation/CustomNav';
+import CustomTabContent from '../CustomNavigation/CustomTabContent';
+import type { DescendantsPageListProps } from '../DescendantsPageList';
+import ExpandOrContractButton from '../ExpandOrContractButton';
 
 import styles from './DescendantsPageListModal.module.scss';
 
-const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
+const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('../DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
 
-const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
+const PageTimeline = dynamic(() => import('../PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
 
 /**
  * DescendantsPageListModalSubstance - Presentation component (all logic here)

+ 18 - 0
apps/app/src/client/components/DescendantsPageListModal/dynamic.tsx

@@ -0,0 +1,18 @@
+import type { JSX } from 'react';
+
+import { useLazyLoader } from '~/client/util/use-lazy-loader';
+import { useDescendantsPageListModalStatus } from '~/states/ui/modal/descendants-page-list';
+
+type DescendantsPageListModalProps = Record<string, unknown>;
+
+export const DescendantsPageListModalLazyLoaded = (): JSX.Element => {
+  const status = useDescendantsPageListModalStatus();
+
+  const DescendantsPageListModal = useLazyLoader<DescendantsPageListModalProps>(
+    'descendants-page-list-modal',
+    () => import('./DescendantsPageListModal').then(mod => ({ default: mod.DescendantsPageListModal })),
+    status?.isOpened ?? false,
+  );
+
+  return DescendantsPageListModal ? <DescendantsPageListModal /> : <></>;
+};

+ 1 - 0
apps/app/src/client/components/DescendantsPageListModal/index.ts

@@ -0,0 +1 @@
+export { DescendantsPageListModalLazyLoaded } from './dynamic';

+ 2 - 4
apps/app/src/client/components/PageDeleteModal.tsx → apps/app/src/client/components/PageDeleteModal/PageDeleteModal.tsx

@@ -19,7 +19,7 @@ import { useSWRxPageInfoForList } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
 
-import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
+import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
 
 const { isTrashPage } = pagePathUtils;
 
@@ -45,7 +45,7 @@ const isIPageInfoForEntityForDeleteModal = (pageInfo: any | undefined): pageInfo
   return pageInfo != null && 'isDeletable' in pageInfo && 'isAbleToDeleteCompletely' in pageInfo;
 };
 
-const PageDeleteModal: FC = () => {
+export const PageDeleteModal: FC = () => {
   const { t } = useTranslation();
   const { isOpened, pages, opts } = usePageDeleteModalStatus() ?? {};
   const { close: closeDeleteModal } = usePageDeleteModalActions();
@@ -328,5 +328,3 @@ const PageDeleteModal: FC = () => {
 
   );
 };
-
-export default PageDeleteModal;

+ 18 - 0
apps/app/src/client/components/PageDeleteModal/dynamic.tsx

@@ -0,0 +1,18 @@
+import type { JSX } from 'react';
+
+import { useLazyLoader } from '~/client/util/use-lazy-loader';
+import { usePageDeleteModalStatus } from '~/states/ui/modal/page-delete';
+
+type PageDeleteModalProps = Record<string, unknown>;
+
+export const PageDeleteModalLazyLoaded = (): JSX.Element => {
+  const status = usePageDeleteModalStatus();
+
+  const PageDeleteModal = useLazyLoader<PageDeleteModalProps>(
+    'page-delete-modal',
+    () => import('./PageDeleteModal').then(mod => ({ default: mod.PageDeleteModal })),
+    status?.isOpened ?? false,
+  );
+
+  return PageDeleteModal ? <PageDeleteModal /> : <></>;
+};

+ 1 - 0
apps/app/src/client/components/PageDeleteModal/index.ts

@@ -0,0 +1 @@
+export { PageDeleteModalLazyLoaded } from './dynamic';

+ 4 - 6
apps/app/src/client/components/PageDuplicateModal.tsx → apps/app/src/client/components/PageDuplicateModal/PageDuplicateModal.tsx

@@ -15,9 +15,9 @@ import { useSiteUrl } from '~/states/global';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
 import { usePageDuplicateModalStatus, usePageDuplicateModalActions } from '~/states/ui/modal/page-duplicate';
 
-import DuplicatePathsTable from './DuplicatedPathsTable';
-import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
-import PagePathAutoComplete from './PagePathAutoComplete';
+import DuplicatePathsTable from '../DuplicatedPathsTable';
+import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
+import PagePathAutoComplete from '../PagePathAutoComplete';
 
 /**
  * PageDuplicateModalSubstance - Heavy processing component (rendered only when modal is open)
@@ -299,7 +299,7 @@ const PageDuplicateModalSubstance: React.FC = () => {
 /**
  * PageDuplicateModal - Container component (lightweight, always rendered)
  */
-const PageDuplicateModal = (): React.JSX.Element => {
+export const PageDuplicateModal = (): React.JSX.Element => {
   const { isOpened } = usePageDuplicateModalStatus();
   const { close: closeDuplicateModal } = usePageDuplicateModalActions();
 
@@ -309,5 +309,3 @@ const PageDuplicateModal = (): React.JSX.Element => {
     </Modal>
   );
 };
-
-export default PageDuplicateModal;

+ 18 - 0
apps/app/src/client/components/PageDuplicateModal/dynamic.tsx

@@ -0,0 +1,18 @@
+import type { JSX } from 'react';
+
+import { useLazyLoader } from '~/client/util/use-lazy-loader';
+import { usePageDuplicateModalStatus } from '~/states/ui/modal/page-duplicate';
+
+type PageDuplicateModalProps = Record<string, unknown>;
+
+export const PageDuplicateModalLazyLoaded = (): JSX.Element => {
+  const status = usePageDuplicateModalStatus();
+
+  const PageDuplicateModal = useLazyLoader<PageDuplicateModalProps>(
+    'page-duplicate-modal',
+    () => import('./PageDuplicateModal').then(mod => ({ default: mod.PageDuplicateModal })),
+    status?.isOpened ?? false,
+  );
+
+  return PageDuplicateModal ? <PageDuplicateModal /> : <></>;
+};

+ 1 - 0
apps/app/src/client/components/PageDuplicateModal/index.ts

@@ -0,0 +1 @@
+export { PageDuplicateModalLazyLoaded } from './dynamic';

+ 4 - 6
apps/app/src/client/components/PageRenameModal.tsx → apps/app/src/client/components/PageRenameModal/PageRenameModal.tsx

@@ -18,9 +18,9 @@ import { isSearchServiceReachableAtom } from '~/states/server-configurations';
 import { usePageRenameModalStatus, usePageRenameModalActions } from '~/states/ui/modal/page-rename';
 import { useSWRxPageInfo } from '~/stores/page';
 
-import DuplicatedPathsTable from './DuplicatedPathsTable';
-import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
-import PagePathAutoComplete from './PagePathAutoComplete';
+import DuplicatedPathsTable from '../DuplicatedPathsTable';
+import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
+import PagePathAutoComplete from '../PagePathAutoComplete';
 
 const isV5Compatible = (meta: unknown): boolean => {
   return isIPageInfoForEntity(meta) ? meta.isV5Compatible : true;
@@ -371,7 +371,7 @@ const PageRenameModalSubstance: React.FC = () => {
 /**
  * PageRenameModal - Container component (lightweight, always rendered)
  */
-const PageRenameModal = (): React.JSX.Element => {
+export const PageRenameModal = (): React.JSX.Element => {
   const { isOpened } = usePageRenameModalStatus();
   const { close: closeRenameModal } = usePageRenameModalActions();
 
@@ -381,5 +381,3 @@ const PageRenameModal = (): React.JSX.Element => {
     </Modal>
   );
 };
-
-export default PageRenameModal;

+ 18 - 0
apps/app/src/client/components/PageRenameModal/dynamic.tsx

@@ -0,0 +1,18 @@
+import type { JSX } from 'react';
+
+import { useLazyLoader } from '~/client/util/use-lazy-loader';
+import { usePageRenameModalStatus } from '~/states/ui/modal/page-rename';
+
+type PageRenameModalProps = Record<string, unknown>;
+
+export const PageRenameModalLazyLoaded = (): JSX.Element => {
+  const status = usePageRenameModalStatus();
+
+  const PageRenameModal = useLazyLoader<PageRenameModalProps>(
+    'page-rename-modal',
+    () => import('./PageRenameModal').then(mod => ({ default: mod.PageRenameModal })),
+    status?.isOpened ?? false,
+  );
+
+  return PageRenameModal ? <PageRenameModal /> : <></>;
+};

+ 1 - 0
apps/app/src/client/components/PageRenameModal/index.ts

@@ -0,0 +1 @@
+export { PageRenameModalLazyLoaded } from './dynamic';

+ 9 - 6
apps/app/src/components/Layout/BasicLayout.tsx

@@ -6,6 +6,12 @@ import dynamic from 'next/dynamic';
 // eslint-disable-next-line no-restricted-imports
 import { PageAccessoriesModalLazyLoaded } from '~/client/components/PageAccessoriesModal';
 // eslint-disable-next-line no-restricted-imports
+import { PageDeleteModalLazyLoaded } from '~/client/components/PageDeleteModal';
+// eslint-disable-next-line no-restricted-imports
+import { PageDuplicateModalLazyLoaded } from '~/client/components/PageDuplicateModal';
+// eslint-disable-next-line no-restricted-imports
+import { PageRenameModalLazyLoaded } from '~/client/components/PageRenameModal';
+// eslint-disable-next-line no-restricted-imports
 import { ShortcutsModalLazyLoaded } from '~/client/components/ShortcutsModal';
 
 import { RawLayout } from './RawLayout';
@@ -34,9 +40,6 @@ const SystemVersion = dynamic(() => import('~/client/components/SystemVersion'),
 const PutbackPageModal = dynamic(() => import('~/client/components/PutbackPageModal'), { ssr: false });
 // Page modals
 const PageCreateModal = dynamic(() => import('~/client/components/PageCreateModal'), { ssr: false });
-const PageDuplicateModal = dynamic(() => import('~/client/components/PageDuplicateModal'), { ssr: false });
-const PageDeleteModal = dynamic(() => import('~/client/components/PageDeleteModal'), { ssr: false });
-const PageRenameModal = dynamic(() => import('~/client/components/PageRenameModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('~/client/components/PagePresentationModal'), { ssr: false });
 const GrantedGroupsInheritanceSelectModal = dynamic(() => import('~/client/components/GrantedGroupsInheritanceSelectModal'), { ssr: false });
 const DeleteBookmarkFolderModal = dynamic(
@@ -77,9 +80,9 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
       <SearchModal />
 
       <PageCreateModal />
-      <PageDuplicateModal />
-      <PageDeleteModal />
-      <PageRenameModal />
+      <PageDuplicateModalLazyLoaded />
+      <PageDeleteModalLazyLoaded />
+      <PageRenameModalLazyLoaded />
       <PageAccessoriesModalLazyLoaded />
       <DeleteAttachmentModal />
       <DeleteBookmarkFolderModal />

+ 2 - 4
apps/app/src/pages/[[...path]]/index.page.tsx

@@ -10,6 +10,8 @@ import type {
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 
+// eslint-disable-next-line no-restricted-imports
+import { DescendantsPageListModal } from '~/client/components/DescendantsPageListModal/DescendantsPageListModal';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { PageView } from '~/components/PageView/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
@@ -57,10 +59,6 @@ const DisplaySwitcher = dynamic(() => import('~/client/components/Page/DisplaySw
 const PageStatusAlert = dynamic(() => import('~/client/components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
 
 const UnsavedAlertDialog = dynamic(() => import('~/client/components/UnsavedAlertDialog'), { ssr: false });
-const DescendantsPageListModal = dynamic(
-  () => import('~/client/components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal),
-  { ssr: false },
-);
 const DrawioModal = dynamic(() => import('~/client/components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('~/client/components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
 const TemplateModal = dynamic(() => import('~/client/components/TemplateModal').then(mod => mod.TemplateModal), { ssr: false });