358 lines
17 KiB
Markdown
Executable File
358 lines
17 KiB
Markdown
Executable File
# 2026-04-10 작업일지
|
|
|
|
## 오늘 작업
|
|
|
|
- `react-router-dom` 기반 앱 셸로 메인 화면을 재구성하고, 상단 메뉴와 사이드바 선택이 URL 경로와 직접 연결되도록 정리했습니다.
|
|
- `AppShell`, `MainLayout`, `routes.tsx`를 추가해 `Docs / APIs / Plans / Chat / Play`를 페이지 단위로 분리했습니다.
|
|
- `MainContent`는 공통 창 레이어와 콘텐츠 컨테이너 역할만 남기고, 실제 화면 렌더링은 각 페이지 컴포넌트로 이동시켰습니다.
|
|
- 저장 레이아웃 상세 화면은 `savedLayoutViewId` 전용 fit 모드로 보정해, 상단 바를 포함한 전체 구성을 한 화면 안에 맞춰 보이도록 수정했습니다.
|
|
- `Text Memo Widget`의 삭제 버튼이 실제로 동작하도록 `Modal.useModal()` 기반 확인 플로우를 연결했습니다.
|
|
- 오늘 증적용으로 `APIs / Widgets` 전체 화면 1장과 `Text Memo Widget` 부분 화면 1장을 `docs/assets/worklogs/2026-04-10/`에 저장했습니다.
|
|
- 오늘자 작업일지 문서를 새로 작성하고, 소스 탭에서 `전체소스 / diff` 전환에 대응할 수 있도록 파일별 raw diff 근거와 전체 변경 파일 목록을 정리했습니다.
|
|
|
|
## 이슈 및 해결
|
|
|
|
- 기존 `MainView` 한 파일에 메뉴 상태, 검색 옵션, 문서/플랜/채팅/플레이 렌더링이 모두 얽혀 있어 경로 기반 탐색과 확장이 어려웠습니다.
|
|
- `AppShell + MainLayout + pages/* + routes.tsx` 구조로 나누고, 라우팅 파생 상태는 `parseRoute()`와 `MainLayoutContext`에서만 계산하도록 정리했습니다.
|
|
- 저장 레이아웃 전용 화면은 내부 콘텐츠 높이와 스크롤 상태에 따라 미리보기가 잘리거나 빈 여백이 생겼습니다.
|
|
- `useLayoutEffect`, `ResizeObserver`, `MutationObserver`로 실제 렌더 크기를 다시 재고 `savedLayoutFitScale`을 계산해 fit 렌더를 안정화했습니다.
|
|
- 메모 위젯 삭제 버튼은 정적 모달 호출과 컨텍스트 미연결 상태 때문에 클릭해도 확인창이 뜨지 않는 문제가 있었습니다.
|
|
- `Modal.useModal()`을 도입하고 `modalContextHolder`를 위젯 루트에 넣어 저장 메모와 작성 중 초안 모두 삭제 가능하게 바꿨습니다.
|
|
- 이 작업 환경에서는 Git이 `dubious ownership`으로 차단되고 글로벌 `safe.directory` 등록도 실패했습니다.
|
|
- 글로벌 설정 대신 모든 Git 조회에 `git -c safe.directory=/workspace/auto_codex/repo ...`를 사용해 증적 수집을 계속 진행했습니다.
|
|
|
|
## 결정 사항
|
|
|
|
- 메인 앱 진입은 상태 기반 조건 분기보다 URL 라우팅을 우선 기준으로 유지합니다.
|
|
- 공통 레이아웃 상태는 `MainLayoutContext`로 전달하고, 페이지별 렌더링 책임은 `pages/*`로 분리합니다.
|
|
- 저장 레이아웃 상세 뷰는 스크롤보다 fit 우선 미리보기 규칙을 사용합니다.
|
|
- 메모 삭제는 즉시 삭제하지 않고 항상 확인 모달을 거치게 유지합니다.
|
|
- 오늘 대표 스크린샷은 `APIs / Widgets` 전체 화면, 부분 스크린샷은 `Text Memo Widget`으로 고정합니다.
|
|
|
|
## 상세 작업 내역
|
|
|
|
- 오늘 Git 이력은 `WindowUI` 최소화/헤더 정리, 메모 삭제 플로우 복구, 저장 레이아웃 fit 모드 보정, 앱 셸 라우팅 분리 순서로 누적됐고 최종적으로 `release -> main` 동기화까지 진행됐습니다.
|
|
- 라우팅 개편은 `MainView`에 모여 있던 메뉴/문서/플랜/채팅/플레이 조건 분기를 걷어내고, `AppShell` 아래에서 `MainLayout`이 공통 상태를 제공한 뒤 각 페이지가 자기 화면만 렌더링하도록 재조정한 작업입니다.
|
|
- 검색 옵션 빌드도 같은 라우팅 구조를 따라가도록 재작성해서, 검색 결과가 단순 상태 변경이 아니라 실제 경로 전환과 포커스 이동을 함께 수행하도록 맞췄습니다.
|
|
- 저장 레이아웃 보정은 단순 CSS 스케일링이 아니라 실제 콘텐츠 크기를 측정해 viewport 안에 맞추는 방식으로 바꿨고, 관련 overflow 규칙도 같이 조정했습니다.
|
|
- 메모 위젯은 삭제 버튼 활성화와 확인 모달 흐름을 분리해, 저장된 메모와 작성 중 초안을 같은 UX 안에서 처리하도록 정리했습니다.
|
|
- 오늘 작업일지 작성 단계에서는 Git 로그, diff, 빌드 결과, 실제 Playwright 캡처를 함께 확인해서 문서와 스크린샷 링크를 최신 상태로 맞췄습니다.
|
|
|
|
## 스크린샷
|
|
|
|

|
|
|
|
- `APIs / Widgets` 전체 화면 캡처입니다. 라우팅 기반 앱 셸 아래에서 위젯 갤러리 전체 내역이 한 번에 보이도록 full-page로 저장했습니다.
|
|
|
|

|
|
|
|
- `Text Memo Widget` 부분 캡처입니다. 오늘 복구한 메모 입력/삭제 흐름을 위젯 단위로 바로 확인할 수 있도록 별도로 남겼습니다.
|
|
|
|
## 소스
|
|
|
|
### 파일 1: `src/app/main/AppShell.tsx`
|
|
|
|
- 라우터 엔트리와 페이지 분리의 기준점입니다.
|
|
|
|
```diff
|
|
+import { Navigate, Route, Routes } from 'react-router-dom';
|
|
+import { MainLayout } from './layout/MainLayout';
|
|
+import { ApisPage } from './pages/ApisPage';
|
|
+import { ChatPage } from './pages/ChatPage';
|
|
+import { DocsPage } from './pages/DocsPage';
|
|
+import { PlansPage } from './pages/PlansPage';
|
|
+import { PlayPage } from './pages/PlayPage';
|
|
+<Route path="/" element={<MainLayout />}>
|
|
+ <Route path="docs/:folder" element={<DocsPage />} />
|
|
+ <Route path="apis/:section" element={<ApisPage />} />
|
|
+ <Route path="plans/:section" element={<PlansPage />} />
|
|
+ <Route path="chat/:section" element={<ChatPage />} />
|
|
+ <Route path="play/layout-record/:layoutId" element={<PlayPage />} />
|
|
+</Route>
|
|
```
|
|
|
|
### 파일 2: `src/app/main/layout/MainLayout.tsx`
|
|
|
|
- 기존 `MainView`의 거대한 상태 조합을 라우팅 기반 공통 레이아웃으로 옮겼습니다.
|
|
|
|
```diff
|
|
+import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
|
+function parseRoute(pathname: string) {
|
|
+ if (top === 'docs') { ... }
|
|
+ if (top === 'apis' && (first === 'components' || first === 'widgets')) { ... }
|
|
+ if (top === 'play' && first === 'layout-record' && second) { ... }
|
|
+}
|
|
+const routeState = useMemo(() => parseRoute(location.pathname), [location.pathname]);
|
|
+const layoutData = useMainLayoutData();
|
|
+<MainLayoutContextProvider value={...}>
|
|
+ <MainHeader ... />
|
|
+ <MainSidebar ... />
|
|
+ <MainContent contentExpanded={contentExpanded} onToggleContentExpanded={...}>
|
|
+ <Outlet />
|
|
+ </MainContent>
|
|
+</MainLayoutContextProvider>
|
|
```
|
|
|
|
### 파일 3: `src/app/main/routes.tsx`
|
|
|
|
- 메뉴 라벨, 경로 빌더, 사이드바 공개 타입을 한곳에 모았습니다.
|
|
|
|
```diff
|
|
+export type TopMenuKey = 'docs' | 'apis' | 'plans' | 'chat' | 'play';
|
|
+export type PlanSectionKey = PlanFilterStatus | 'release' | 'charts' | 'history';
|
|
+export const DOCS_DEFAULT_FOLDER = 'worklogs';
|
|
+export function buildDocsPath(folder = DOCS_DEFAULT_FOLDER) {
|
|
+ return `/docs/${folder}`;
|
|
+}
|
|
+export function buildPlansPath(section: PlanSectionKey = 'all') {
|
|
+ return `/plans/${section}`;
|
|
+}
|
|
+export function buildPlayMenuItems(savedLayouts: Array<{ id: string; name: string }>): MenuProps['items'] {
|
|
+ return [{ key: 'play-group', children: [...] }];
|
|
+}
|
|
```
|
|
|
|
### 파일 4: `src/app/main/MainContent.tsx`
|
|
|
|
- 공통 창 레이어만 남기고 화면별 조건 분기를 페이지 컴포넌트로 밀어냈습니다.
|
|
|
|
```diff
|
|
-import { Button, Card, Layout, Modal, Space, Typography } from 'antd';
|
|
+import { Button, Layout, Modal, Space, Typography } from 'antd';
|
|
-import { MarkdownPreviewCard } from '../../components/markdownPreview';
|
|
+import { useMainLayoutContext } from './layout/MainLayoutContext';
|
|
-export function MainContent({ activeTopMenu, selectedApiMenu, selectedDocsMenu, ... }: MainContentProps) {
|
|
+export function MainContent({ contentExpanded, onToggleContentExpanded, children }: MainContentProps) {
|
|
+ const { componentSampleEntries, widgetSampleEntries, componentSamples, widgetSamples, initialSelectedPlanId, initialSelectedWorkId } =
|
|
+ useMainLayoutContext();
|
|
- <div className={activeTopMenu === 'chat' ? 'app-main-layout app-main-layout--single' : 'app-main-layout'}>
|
|
- {activeTopMenu === 'docs' ? ... : activeTopMenu === 'plans' ? ... : ...}
|
|
- </div>
|
|
+ <div className="app-main-layout">{children}</div>
|
|
```
|
|
|
|
### 파일 5: `src/app/main/MainSidebar.tsx`
|
|
|
|
- 소개 영역과 API 메뉴 선택 타입을 공통 라우팅 구조에 맞게 정리했습니다.
|
|
|
|
```diff
|
|
+ introColor,
|
|
+ introTag,
|
|
+ introDescription,
|
|
- const activeTagColor = isDocsGroup ? 'gold' : activeTopMenu === 'play' ? 'cyan' : 'green';
|
|
- <Tag color={activeTagColor}>...</Tag>
|
|
+ <Tag color={introColor}>{introTag}</Tag>
|
|
+ <Text type="secondary">{introDescription}</Text>
|
|
- onSelectApiMenu(key);
|
|
+ onSelectApiMenu(key as MainSidebarProps['selectedApiMenu']);
|
|
```
|
|
|
|
### 파일 6: `src/app/main/layout/MainLayoutContext.ts`
|
|
|
|
- 페이지와 공통 레이어가 같은 상태를 공유하도록 컨텍스트를 신설했습니다.
|
|
|
|
```diff
|
|
+export type MainLayoutContextValue = {
|
|
+ topMenu: TopMenuKey;
|
|
+ selectedDocsMenu: string;
|
|
+ selectedApiMenu: ApiSectionKey;
|
|
+ selectedPlanMenu: PlanSectionKey;
|
|
+ selectedChatMenu: ChatSectionKey;
|
|
+ selectedPlayMenu: PlaySidebarKey;
|
|
+ searchOptions: SearchKeywordOption[];
|
|
+};
|
|
+const MainLayoutContext = createContext<MainLayoutContextValue | null>(null);
|
|
+export function useMainLayoutContext() { ... }
|
|
```
|
|
|
|
### 파일 7: `src/app/main/layout/buildSearchOptions.ts`
|
|
|
|
- 검색 결과가 상태 변경이 아니라 실제 라우팅 전환을 수행하도록 재작성했습니다.
|
|
|
|
```diff
|
|
+import { buildApisPath, buildChatPath, buildDocsPath, buildPlansPath, buildPlayPath } from '../routes';
|
|
+onSelect: () => {
|
|
+ requestPlanQuickFilter(null);
|
|
+ navigateTo(buildApisPath('widgets'));
|
|
+ setFocusedComponentId(null);
|
|
+},
|
|
+onSelect: () => {
|
|
+ requestPlanQuickFilter('release-pending-main');
|
|
+ navigateTo(buildPlansPath('release'));
|
|
+ setFocusedComponentId(null);
|
|
+},
|
|
+onSelect: () => {
|
|
+ navigateTo(buildDocsPath(document.folder));
|
|
+ setFocusedComponentId(`doc:${document.id}`);
|
|
+ scrollToElement(`document-preview-${document.id}`);
|
|
+},
|
|
```
|
|
|
|
### 파일 8: `src/app/main/pages/ApisPage.tsx`
|
|
|
|
- APIs 화면 렌더링 책임을 페이지 단위로 분리했습니다.
|
|
|
|
```diff
|
|
+export function ApisPage() {
|
|
+ const { selectedApiMenu, componentSampleEntries, widgetSampleEntries } = useMainLayoutContext();
|
|
+ return (
|
|
+ <div className="app-main-panel">
|
|
+ <Card title={selectedApiMenu === 'components' ? 'APIs / Components' : 'APIs / Widgets'} className="app-main-card" bordered={false}>
|
|
+ {selectedApiMenu === 'components' ? (
|
|
+ <ComponentSamplesLayout entries={componentSampleEntries} excludeComponentIds={HIDDEN_COMPONENT_IDS} />
|
|
+ ) : (
|
|
+ <SampleWidgetsLayout entries={widgetSampleEntries} />
|
|
+ )}
|
|
+ </Card>
|
|
+ </div>
|
|
+ );
|
|
+}
|
|
```
|
|
|
|
### 파일 9: `src/views/play/LayoutPlaygroundView.tsx`
|
|
|
|
- 저장 레이아웃 전용 fit 모드와 실제 크기 측정 로직을 추가했습니다.
|
|
|
|
```diff
|
|
-import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
|
+import { useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
|
+const isSavedLayoutFitMode = Boolean(savedLayoutViewId);
|
|
+const [savedLayoutFitScale, setSavedLayoutFitScale] = useState(1);
|
|
+const [savedLayoutFitSize, setSavedLayoutFitSize] = useState<{ width: number; height: number } | null>(null);
|
|
+const savedLayoutFitViewportRef = useRef<HTMLDivElement | null>(null);
|
|
+const savedLayoutFitContentRef = useRef<HTMLDivElement | null>(null);
|
|
+useLayoutEffect(() => {
|
|
+ const nextScale = Math.min(1, availableWidth / width, availableHeight / height);
|
|
+ setSavedLayoutFitScale(...);
|
|
+}, [savedLayoutViewId, selectedSavedLayoutRecord, sampleEntries.length]);
|
|
+style={{ transform: `scale(${savedLayoutFitScale})`, width: `${savedLayoutFitSize.width}px` }}
|
|
+previewSurface: isSavedLayoutFitMode,
|
|
```
|
|
|
|
### 파일 10: `src/styles.css`
|
|
|
|
- fit 모드에서 overflow와 스크롤바를 억제하는 전용 스타일을 추가했습니다.
|
|
|
|
```diff
|
|
+.layout-playground__fullscreen-shell--saved-fit {
|
|
+ min-height: 0;
|
|
+ overflow: hidden;
|
|
+}
|
|
+.layout-playground__saved-fit-viewport {
|
|
+ display: flex;
|
|
+ align-items: center;
|
|
+ justify-content: center;
|
|
+ overflow: hidden;
|
|
+}
|
|
+.layout-playground__saved-detail--fit * {
|
|
+ scrollbar-width: none;
|
|
+}
|
|
+.layout-playground__saved-detail--fit *::-webkit-scrollbar {
|
|
+ width: 0;
|
|
+ height: 0;
|
|
+}
|
|
```
|
|
|
|
### 파일 11: `src/widgets/text-memo-widget/TextMemoWidget.tsx`
|
|
|
|
- 삭제 버튼 비활성 상태를 해제하고, 확인 모달을 거쳐 실제 삭제되도록 바꿨습니다.
|
|
|
|
```diff
|
|
-import { Button, Empty, Input, message } from 'antd';
|
|
+import { Button, Empty, Input, Modal, message } from 'antd';
|
|
+const [modalApi, modalContextHolder] = Modal.useModal();
|
|
-const handleDelete = () => {
|
|
- void messageApi.info('삭제 기능은 현재 비활성화되어 있습니다.');
|
|
-};
|
|
+const handleDelete = () => {
|
|
+ if (!selectedNote && !hasDraft) {
|
|
+ return;
|
|
+ }
|
|
+ void modalApi.confirm({
|
|
+ title: isDraftOnly ? '작성 중인 메모를 삭제할까요?' : '선택한 메모를 삭제할까요?',
|
|
+ onOk: () => { ... },
|
|
+ });
|
|
+};
|
|
+{modalContextHolder}
|
|
-disabled
|
|
+disabled={!selectedNote && !hasDraft}
|
|
```
|
|
|
|
### 파일 12: `src/App.tsx`, `src/main.tsx`, `src/app/main/index.ts`
|
|
|
|
- 앱 진입점을 라우터 기반 셸로 연결했습니다.
|
|
|
|
```diff
|
|
-import { MainView } from './app/main';
|
|
+import { BrowserRouter } from 'react-router-dom';
|
|
+import { MainView } from './app/main';
|
|
+<BrowserRouter>
|
|
+ <MainView />
|
|
+</BrowserRouter>
|
|
-export { MainView } from './MainView';
|
|
+export { AppShell } from './AppShell';
|
|
+export { MainView } from './MainView';
|
|
-export function MainView() { ...legacy layout... }
|
|
+export function MainView() {
|
|
+ return <AppShell />;
|
|
+}
|
|
```
|
|
|
|
## 실행 커맨드
|
|
|
|
```bash
|
|
find docs/worklogs docs/assets/worklogs -type f | sort
|
|
grep -RIn "작업일지\|스크린샷\|소스 탭\|실행 커맨드\|상세형" docs/worklogs --include='*.md'
|
|
git -c safe.directory=/workspace/auto_codex/repo status --short --branch
|
|
sed -n '1,260p' docs/worklogs/2026-04-09.md
|
|
sed -n '1,260p' docs/templates/worklog-template.md
|
|
find scripts -maxdepth 1 -type f | sort
|
|
git -c safe.directory=/workspace/auto_codex/repo log --since='2026-04-10 00:00:00' --until='2026-04-10 23:59:59' --stat --oneline
|
|
git -c safe.directory=/workspace/auto_codex/repo log --since='2026-04-10 00:00:00' --until='2026-04-10 23:59:59' --name-only --pretty=format: | sed '/^$/d' | sort | uniq
|
|
git -c safe.directory=/workspace/auto_codex/repo diff --stat 0b105f8..HEAD -- src/app/main/MainView.tsx src/app/main/layout/MainLayout.tsx src/app/main/routes.tsx src/app/main/MainContent.tsx src/widgets/text-memo-widget/TextMemoWidget.tsx src/components/window/WindowUI.tsx src/styles.css src/views/play/LayoutPlaygroundView.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/app/main/MainView.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/app/main/layout/MainLayout.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/widgets/text-memo-widget/TextMemoWidget.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/app/main/routes.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/app/main/MainContent.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/views/play/LayoutPlaygroundView.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/app/main/AppShell.tsx src/app/main/MainSidebar.tsx src/app/main/layout/MainLayoutContext.ts src/app/main/layout/buildSearchOptions.ts src/app/main/layout/useMainLayoutData.ts src/app/main/pages/ApisPage.tsx src/app/main/pages/ChatPage.tsx src/app/main/pages/DocsPage.tsx src/app/main/pages/PlansPage.tsx src/main.tsx
|
|
git -c safe.directory=/workspace/auto_codex/repo diff 0b105f8..HEAD -- src/components/window/WindowUI.tsx src/components/window/WindowUI.css src/widgets/text-memo-widget/TextMemoWidget.css src/styles.css
|
|
git -c safe.directory=/workspace/auto_codex/repo diff --name-status 0b105f8..HEAD
|
|
npm run build:app
|
|
PORT=4173 node scripts/serve-app-dist.mjs
|
|
node --input-type=module <<'EOF'
|
|
import { chromium } from 'playwright';
|
|
// APIs / Widgets full-page screenshot and text memo widget crop capture
|
|
EOF
|
|
```
|
|
|
|
## 변경/신규 파일
|
|
|
|
- A docs/worklogs/2026-04-10.md
|
|
- A docs/assets/worklogs/2026-04-10/feature-apis-widgets-full.png
|
|
- A docs/assets/worklogs/2026-04-10/widget-text-memo.png
|
|
- M package-lock.json
|
|
- M package.json
|
|
- M src/App.tsx
|
|
- A src/app/main/AppShell.tsx
|
|
- M src/app/main/MainContent.tsx
|
|
- M src/app/main/MainSidebar.tsx
|
|
- M src/app/main/MainView.tsx
|
|
- M src/app/main/index.ts
|
|
- A src/app/main/layout/MainLayout.tsx
|
|
- A src/app/main/layout/MainLayoutContext.ts
|
|
- A src/app/main/layout/buildSearchOptions.ts
|
|
- A src/app/main/layout/useMainLayoutData.ts
|
|
- A src/app/main/pages/ApisPage.tsx
|
|
- A src/app/main/pages/ChatPage.tsx
|
|
- A src/app/main/pages/DocsPage.tsx
|
|
- A src/app/main/pages/PlansPage.tsx
|
|
- A src/app/main/pages/PlayPage.tsx
|
|
- A src/app/main/routes.tsx
|
|
- M src/app/main/types.ts
|
|
- M src/main.tsx
|
|
- M src/styles.css
|
|
- M src/views/play/LayoutPlaygroundView.tsx
|
|
- M src/widgets/text-memo-widget/TextMemoWidget.tsx
|