소스 검색

Dock Folder README as a collapsible right rail instead of a top block (#2520)

The README panel rendered full-width above the file grid, pushing model
files below the fold with no page scroll to get past it. On lg+ it now
docks as a fixed-width right-hand column beside the list (own full-height
scroll); on mobile it stacks on top and the page scrolls. Added a collapse
toggle (thin strip / slim bar + one-click reopen) with the choice persisted
to localStorage. New i18n keys readme.show/hide/label across 11 locales.
maziggy 1 개월 전
부모
커밋
1603f52a07

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
CHANGELOG.md


+ 56 - 1
frontend/src/__tests__/components/FolderReadmePanel.test.tsx

@@ -2,14 +2,23 @@
  * Tests for FolderReadmePanel (#1268).
  * Tests for FolderReadmePanel (#1268).
  */
  */
 
 
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
 import { screen, waitFor } from '@testing-library/react';
 import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import { http, HttpResponse } from 'msw';
 import { http, HttpResponse } from 'msw';
 import { render } from '../utils';
 import { render } from '../utils';
 import { FolderReadmePanel } from '../../components/FolderReadmePanel';
 import { FolderReadmePanel } from '../../components/FolderReadmePanel';
 import { server } from '../mocks/server';
 import { server } from '../mocks/server';
 
 
 describe('FolderReadmePanel', () => {
 describe('FolderReadmePanel', () => {
+  beforeEach(() => {
+    // localStorage is a vi.fn() mock in setup.ts (no real persistence) and
+    // calls/return values leak across tests — reset it so each test starts
+    // with the collapse preference unset (expanded).
+    vi.mocked(localStorage.getItem).mockReturnValue(null);
+    vi.mocked(localStorage.setItem).mockClear();
+  });
+
   it('renders nothing when the folder has no markdown (404)', async () => {
   it('renders nothing when the folder has no markdown (404)', async () => {
     server.use(
     server.use(
       http.get('/api/v1/library/folders/:id/readme', () =>
       http.get('/api/v1/library/folders/:id/readme', () =>
@@ -55,4 +64,50 @@ describe('FolderReadmePanel', () => {
     render(<FolderReadmePanel folderId={7} />);
     render(<FolderReadmePanel folderId={7} />);
     expect(await screen.findByText('Truncated')).toBeInTheDocument();
     expect(await screen.findByText('Truncated')).toBeInTheDocument();
   });
   });
+
+  it('collapses to a reopen control and hides the content, persisting the choice (#2520)', async () => {
+    server.use(
+      http.get('/api/v1/library/folders/:id/readme', () =>
+        HttpResponse.json({
+          filename: 'README.md',
+          content: '# Robot model\n\nA cute robot.',
+          truncated: false,
+        }),
+      ),
+    );
+    const user = userEvent.setup();
+    render(<FolderReadmePanel folderId={99} />);
+
+    // Expanded by default: content visible.
+    expect(await screen.findByRole('heading', { name: 'Robot model' })).toBeInTheDocument();
+
+    // Collapse hides the markdown body...
+    await user.click(screen.getByRole('button', { name: 'Hide README' }));
+    await waitFor(() => {
+      expect(screen.queryByRole('heading', { name: 'Robot model' })).not.toBeInTheDocument();
+    });
+    // ...and offers a reopen control + persists the choice.
+    expect(screen.getAllByRole('button', { name: 'Show README' }).length).toBeGreaterThan(0);
+    expect(localStorage.setItem).toHaveBeenCalledWith('fileManager.readmeCollapsed', '1');
+  });
+
+  it('starts collapsed when the persisted preference is collapsed (#2520)', async () => {
+    vi.mocked(localStorage.getItem).mockImplementation((k) =>
+      k === 'fileManager.readmeCollapsed' ? '1' : null,
+    );
+    server.use(
+      http.get('/api/v1/library/folders/:id/readme', () =>
+        HttpResponse.json({
+          filename: 'README.md',
+          content: '# Robot model\n\nA cute robot.',
+          truncated: false,
+        }),
+      ),
+    );
+    render(<FolderReadmePanel folderId={5} />);
+
+    // Reopen control is present; the markdown body is not rendered.
+    expect((await screen.findAllByRole('button', { name: 'Show README' })).length).toBeGreaterThan(0);
+    expect(screen.queryByRole('heading', { name: 'Robot model' })).not.toBeInTheDocument();
+  });
 });
 });

+ 108 - 61
frontend/src/components/FolderReadmePanel.tsx

@@ -1,7 +1,7 @@
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { useQuery } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { ChevronDown, ChevronUp, FileText } from 'lucide-react';
+import { FileText, PanelRightClose, PanelRightOpen } from 'lucide-react';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
 import remarkGfm from 'remark-gfm';
 import remarkGfm from 'remark-gfm';
 
 
@@ -11,14 +11,31 @@ interface FolderReadmePanelProps {
   folderId: number;
   folderId: number;
 }
 }
 
 
+// Persist the collapsed choice so hiding the README once keeps it hidden
+// across folder switches and page reloads (#2520 item 2).
+const COLLAPSE_STORAGE_KEY = 'fileManager.readmeCollapsed';
+
 /**
 /**
- * Side panel that renders a `.md` file from the selected folder (#1268).
- * Hidden when the folder has no markdown file. Disables raw HTML and links
- * stay text-only — same posture as the print-archive note panel.
+ * Markdown panel for the selected folder (#1268).
+ *
+ * Docks as a collapsible right-hand rail on wide screens so the README sits
+ * *beside* the file list instead of pushing it down and eating vertical
+ * space (#2520 item 2); on narrow screens it stacks above the list
+ * (`order-first`) where the page itself scrolls. Collapsing shrinks it to a
+ * thin strip (desktop) / slim bar (mobile) with a one-click reopen, and the
+ * choice is persisted. Auto-hidden when the folder has no markdown. Raw HTML
+ * is disabled and links stay text-only — same posture as the print-archive
+ * note panel.
  */
  */
 export function FolderReadmePanel({ folderId }: FolderReadmePanelProps) {
 export function FolderReadmePanel({ folderId }: FolderReadmePanelProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const [collapsed, setCollapsed] = useState(false);
+  const [collapsed, setCollapsed] = useState<boolean>(
+    () => localStorage.getItem(COLLAPSE_STORAGE_KEY) === '1',
+  );
+
+  useEffect(() => {
+    localStorage.setItem(COLLAPSE_STORAGE_KEY, collapsed ? '1' : '0');
+  }, [collapsed]);
 
 
   const { data, isLoading, error } = useQuery({
   const { data, isLoading, error } = useQuery({
     queryKey: ['folder-readme', folderId],
     queryKey: ['folder-readme', folderId],
@@ -29,13 +46,41 @@ export function FolderReadmePanel({ folderId }: FolderReadmePanelProps) {
 
 
   if (isLoading || error || !data) return null;
   if (isLoading || error || !data) return null;
 
 
+  if (collapsed) {
+    return (
+      <div className="mb-2 lg:mb-0 order-first lg:order-none lg:w-10 lg:flex-shrink-0 lg:h-full">
+        {/* Mobile: a slim horizontal bar that reopens the panel. */}
+        <button
+          type="button"
+          onClick={() => setCollapsed(false)}
+          title={t('fileManager.readme.show')}
+          aria-label={t('fileManager.readme.show')}
+          className="flex lg:hidden w-full items-center gap-2 px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg hover:bg-bambu-dark/40 transition-colors"
+        >
+          <FileText className="w-4 h-4 text-bambu-green flex-shrink-0" />
+          <span className="text-sm font-medium text-white truncate">{data.filename}</span>
+          <PanelRightOpen className="w-4 h-4 text-bambu-gray flex-shrink-0 ml-auto" />
+        </button>
+        {/* Desktop: a thin vertical strip with a reopen button. */}
+        <button
+          type="button"
+          onClick={() => setCollapsed(false)}
+          title={t('fileManager.readme.show')}
+          aria-label={t('fileManager.readme.show')}
+          className="hidden lg:flex h-full w-10 flex-col items-center gap-2 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg hover:bg-bambu-dark/40 transition-colors"
+        >
+          <PanelRightOpen className="w-4 h-4 text-bambu-green flex-shrink-0" />
+          <span className="text-xs font-medium text-bambu-gray [writing-mode:vertical-rl] rotate-180 select-none">
+            {t('fileManager.readme.label')}
+          </span>
+        </button>
+      </div>
+    );
+  }
+
   return (
   return (
-    <div className="mb-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg overflow-hidden">
-      <button
-        type="button"
-        onClick={() => setCollapsed((v) => !v)}
-        className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left hover:bg-bambu-dark/40 transition-colors"
-      >
+    <div className="mb-4 lg:mb-0 order-first lg:order-none lg:w-80 xl:w-96 lg:flex-shrink-0 lg:h-full flex flex-col bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg overflow-hidden">
+      <div className="flex items-center justify-between gap-2 px-3 py-2">
         <div className="flex items-center gap-2 min-w-0">
         <div className="flex items-center gap-2 min-w-0">
           <FileText className="w-4 h-4 text-bambu-green flex-shrink-0" />
           <FileText className="w-4 h-4 text-bambu-green flex-shrink-0" />
           <span className="text-sm font-medium text-white truncate" title={data.filename}>
           <span className="text-sm font-medium text-white truncate" title={data.filename}>
@@ -47,55 +92,57 @@ export function FolderReadmePanel({ folderId }: FolderReadmePanelProps) {
             </span>
             </span>
           )}
           )}
         </div>
         </div>
-        {collapsed ? (
-          <ChevronDown className="w-4 h-4 text-bambu-gray flex-shrink-0" />
-        ) : (
-          <ChevronUp className="w-4 h-4 text-bambu-gray flex-shrink-0" />
-        )}
-      </button>
-      {!collapsed && (
-        <div className="px-4 py-3 border-t border-bambu-dark-tertiary max-h-96 overflow-y-auto text-sm text-bambu-gray-light leading-relaxed space-y-2">
-          <ReactMarkdown
-            remarkPlugins={[remarkGfm]}
-            components={{
-              h1: ({ children }) => <h1 className="text-lg font-semibold text-white mt-2 mb-1">{children}</h1>,
-              h2: ({ children }) => <h2 className="text-base font-semibold text-white mt-2 mb-1">{children}</h2>,
-              h3: ({ children }) => <h3 className="text-sm font-semibold text-white mt-2 mb-1">{children}</h3>,
-              p: ({ children }) => <p className="my-1">{children}</p>,
-              ul: ({ children }) => <ul className="list-disc list-inside space-y-0.5 ml-2">{children}</ul>,
-              ol: ({ children }) => <ol className="list-decimal list-inside space-y-0.5 ml-2">{children}</ol>,
-              li: ({ children }) => <li>{children}</li>,
-              code: ({ children, ...props }) => {
-                const inline = !(props as { className?: string }).className;
-                return inline ? (
-                  <code className="px-1 py-0.5 bg-bambu-dark rounded text-xs font-mono text-bambu-green">{children}</code>
-                ) : (
-                  <code className="block p-2 bg-bambu-dark rounded text-xs font-mono text-bambu-gray-light overflow-x-auto">{children}</code>
-                );
-              },
-              pre: ({ children }) => <pre className="my-2">{children}</pre>,
-              blockquote: ({ children }) => (
-                <blockquote className="border-l-2 border-bambu-dark-tertiary pl-3 text-bambu-gray italic">{children}</blockquote>
-              ),
-              a: ({ children, href }) => (
-                <a href={href} target="_blank" rel="noopener noreferrer" className="text-bambu-green hover:underline">
-                  {children}
-                </a>
-              ),
-              table: ({ children }) => (
-                <div className="overflow-x-auto">
-                  <table className="min-w-full text-xs border-collapse">{children}</table>
-                </div>
-              ),
-              th: ({ children }) => <th className="border border-bambu-dark-tertiary px-2 py-1 text-left font-semibold text-white">{children}</th>,
-              td: ({ children }) => <td className="border border-bambu-dark-tertiary px-2 py-1">{children}</td>,
-              hr: () => <hr className="border-bambu-dark-tertiary my-2" />,
-            }}
-          >
-            {data.content}
-          </ReactMarkdown>
-        </div>
-      )}
+        <button
+          type="button"
+          onClick={() => setCollapsed(true)}
+          title={t('fileManager.readme.hide')}
+          aria-label={t('fileManager.readme.hide')}
+          className="p-1 rounded text-bambu-gray hover:text-white hover:bg-bambu-dark/40 transition-colors flex-shrink-0"
+        >
+          <PanelRightClose className="w-4 h-4" />
+        </button>
+      </div>
+      <div className="px-4 py-3 border-t border-bambu-dark-tertiary flex-1 overflow-y-auto max-h-96 lg:max-h-none text-sm text-bambu-gray-light leading-relaxed space-y-2">
+        <ReactMarkdown
+          remarkPlugins={[remarkGfm]}
+          components={{
+            h1: ({ children }) => <h1 className="text-lg font-semibold text-white mt-2 mb-1">{children}</h1>,
+            h2: ({ children }) => <h2 className="text-base font-semibold text-white mt-2 mb-1">{children}</h2>,
+            h3: ({ children }) => <h3 className="text-sm font-semibold text-white mt-2 mb-1">{children}</h3>,
+            p: ({ children }) => <p className="my-1">{children}</p>,
+            ul: ({ children }) => <ul className="list-disc list-inside space-y-0.5 ml-2">{children}</ul>,
+            ol: ({ children }) => <ol className="list-decimal list-inside space-y-0.5 ml-2">{children}</ol>,
+            li: ({ children }) => <li>{children}</li>,
+            code: ({ children, ...props }) => {
+              const inline = !(props as { className?: string }).className;
+              return inline ? (
+                <code className="px-1 py-0.5 bg-bambu-dark rounded text-xs font-mono text-bambu-green">{children}</code>
+              ) : (
+                <code className="block p-2 bg-bambu-dark rounded text-xs font-mono text-bambu-gray-light overflow-x-auto">{children}</code>
+              );
+            },
+            pre: ({ children }) => <pre className="my-2">{children}</pre>,
+            blockquote: ({ children }) => (
+              <blockquote className="border-l-2 border-bambu-dark-tertiary pl-3 text-bambu-gray italic">{children}</blockquote>
+            ),
+            a: ({ children, href }) => (
+              <a href={href} target="_blank" rel="noopener noreferrer" className="text-bambu-green hover:underline">
+                {children}
+              </a>
+            ),
+            table: ({ children }) => (
+              <div className="overflow-x-auto">
+                <table className="min-w-full text-xs border-collapse">{children}</table>
+              </div>
+            ),
+            th: ({ children }) => <th className="border border-bambu-dark-tertiary px-2 py-1 text-left font-semibold text-white">{children}</th>,
+            td: ({ children }) => <td className="border border-bambu-dark-tertiary px-2 py-1">{children}</td>,
+            hr: () => <hr className="border-bambu-dark-tertiary my-2" />,
+          }}
+        >
+          {data.content}
+        </ReactMarkdown>
+      </div>
     </div>
     </div>
   );
   );
 }
 }

+ 3 - 0
frontend/src/i18n/locales/de.ts

@@ -3582,6 +3582,9 @@ export default {
     searchSubfoldersHint: 'Inklusive Unterordner',
     searchSubfoldersHint: 'Inklusive Unterordner',
     readme: {
     readme: {
       truncated: 'Gekürzt',
       truncated: 'Gekürzt',
+      show: 'README anzeigen',
+      hide: 'README ausblenden',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Tags',
       title: 'Tags',

+ 3 - 0
frontend/src/i18n/locales/en.ts

@@ -3611,6 +3611,9 @@ export default {
     searchSubfoldersHint: 'Including subfolders',
     searchSubfoldersHint: 'Including subfolders',
     readme: {
     readme: {
       truncated: 'Truncated',
       truncated: 'Truncated',
+      show: 'Show README',
+      hide: 'Hide README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Tags',
       title: 'Tags',

+ 3 - 0
frontend/src/i18n/locales/es.ts

@@ -3585,6 +3585,9 @@ export default {
     searchSubfoldersHint: 'Incluyendo subcarpetas',
     searchSubfoldersHint: 'Incluyendo subcarpetas',
     readme: {
     readme: {
       truncated: 'Truncado',
       truncated: 'Truncado',
+      show: 'Mostrar README',
+      hide: 'Ocultar README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Etiquetas',
       title: 'Etiquetas',

+ 3 - 0
frontend/src/i18n/locales/fr.ts

@@ -3571,6 +3571,9 @@ export default {
     searchSubfoldersHint: 'Sous-dossiers inclus',
     searchSubfoldersHint: 'Sous-dossiers inclus',
     readme: {
     readme: {
       truncated: 'Tronqué',
       truncated: 'Tronqué',
+      show: 'Afficher le README',
+      hide: 'Masquer le README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Étiquettes',
       title: 'Étiquettes',

+ 3 - 0
frontend/src/i18n/locales/it.ts

@@ -3570,6 +3570,9 @@ export default {
     searchSubfoldersHint: 'Sottocartelle incluse',
     searchSubfoldersHint: 'Sottocartelle incluse',
     readme: {
     readme: {
       truncated: 'Troncato',
       truncated: 'Troncato',
+      show: 'Mostra README',
+      hide: 'Nascondi README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Etichette',
       title: 'Etichette',

+ 3 - 0
frontend/src/i18n/locales/ja.ts

@@ -3582,6 +3582,9 @@ export default {
     searchSubfoldersHint: 'サブフォルダーを含む',
     searchSubfoldersHint: 'サブフォルダーを含む',
     readme: {
     readme: {
       truncated: '切り詰め',
       truncated: '切り詰め',
+      show: 'READMEを表示',
+      hide: 'READMEを非表示',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'タグ',
       title: 'タグ',

+ 3 - 0
frontend/src/i18n/locales/ko.ts

@@ -3394,6 +3394,9 @@ export default {
     searchSubfoldersHint: '하위 폴더 포함',
     searchSubfoldersHint: '하위 폴더 포함',
     readme: {
     readme: {
       truncated: '잘림',
       truncated: '잘림',
+      show: 'README 표시',
+      hide: 'README 숨기기',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: '태그',
       title: '태그',

+ 3 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -3570,6 +3570,9 @@ export default {
     searchSubfoldersHint: 'Incluindo subpastas',
     searchSubfoldersHint: 'Incluindo subpastas',
     readme: {
     readme: {
       truncated: 'Truncado',
       truncated: 'Truncado',
+      show: 'Mostrar README',
+      hide: 'Ocultar README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Tags',
       title: 'Tags',

+ 3 - 0
frontend/src/i18n/locales/tr.ts

@@ -3578,6 +3578,9 @@ export default {
     searchSubfoldersHint: 'Alt klasörler dahil',
     searchSubfoldersHint: 'Alt klasörler dahil',
     readme: {
     readme: {
       truncated: 'Kısaltıldı',
       truncated: 'Kısaltıldı',
+      show: 'README’yi göster',
+      hide: 'README’yi gizle',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: 'Etiketler',
       title: 'Etiketler',

+ 3 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -3570,6 +3570,9 @@ export default {
     searchSubfoldersHint: '包含子文件夹',
     searchSubfoldersHint: '包含子文件夹',
     readme: {
     readme: {
       truncated: '已截断',
       truncated: '已截断',
+      show: '显示 README',
+      hide: '隐藏 README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: '标签',
       title: '标签',

+ 3 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -3570,6 +3570,9 @@ export default {
     searchSubfoldersHint: '包含子資料夾',
     searchSubfoldersHint: '包含子資料夾',
     readme: {
     readme: {
       truncated: '已截斷',
       truncated: '已截斷',
+      show: '顯示 README',
+      hide: '隱藏 README',
+      label: 'README',
     },
     },
     tags: {
     tags: {
       title: '標籤',
       title: '標籤',

+ 10 - 4
frontend/src/pages/FileManagerPage.tsx

@@ -1935,11 +1935,13 @@ export function FileManagerPage() {
           </div>
           </div>
         </div>
         </div>
 
 
-        {/* Files area */}
+        {/* Files area + README rail (#2520 item 2). On wide screens the
+            README docks as a collapsible right-hand column (rendered after
+            the files column, below) so it no longer steals vertical space
+            from the file list; on narrow screens it stacks above the list
+            via `order-first` and the page itself scrolls. */}
+        <div className="flex-1 flex flex-col lg:flex-row min-w-0 min-h-0 gap-4 lg:gap-6">
         <div className="flex-1 flex flex-col min-w-0 min-h-0">
         <div className="flex-1 flex flex-col min-w-0 min-h-0">
-          {/* Markdown description panel (#1268) — auto-hides if the folder
-              has no README/description.md so non-users pay no UI cost. */}
-          {selectedFolderId !== null && <FolderReadmePanel folderId={selectedFolderId} />}
           {/* Tag filter rail (#1268). Lists every catalog tag as a togglable
           {/* Tag filter rail (#1268). Lists every catalog tag as a togglable
               chip — active chips are filled green and show an X, inactive
               chip — active chips are filled green and show an X, inactive
               chips are outlined and toggle ON when clicked. Clicking an active
               chips are outlined and toggle ON when clicked. Clicking an active
@@ -2557,6 +2559,10 @@ export function FileManagerPage() {
             </div>
             </div>
           )}
           )}
         </div>
         </div>
+          {/* README rail — collapsible right column on lg+, stacks on top
+              on mobile. See the files-area wrapper comment above (#2520). */}
+          {selectedFolderId !== null && <FolderReadmePanel folderId={selectedFolderId} />}
+        </div>
       </div>
       </div>
 
 
       {/* Modals */}
       {/* Modals */}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
static/assets/index-BC5UbFyP.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
static/assets/index-DvjR9OL3.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
static/assets/index-blSspT6K.css


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CvBwRD12.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-DvjR9OL3.css">
+    <script type="module" crossorigin src="/assets/index-BC5UbFyP.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-blSspT6K.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.