# 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 캡처를 함께 확인해서 문서와 스크린샷 링크를 최신 상태로 맞췄습니다. ## 스크린샷 ![feature-apis-widgets-full](../assets/worklogs/2026-04-10/feature-apis-widgets-full.png) - `APIs / Widgets` 전체 화면 캡처입니다. 라우팅 기반 앱 셸 아래에서 위젯 갤러리 전체 내역이 한 번에 보이도록 full-page로 저장했습니다. ![widget-text-memo](../assets/worklogs/2026-04-10/widget-text-memo.png) - `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'; +}> + } /> + } /> + } /> + } /> + } /> + ``` ### 파일 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(); + + + + + + + ``` ### 파일 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(); -
- {activeTopMenu === 'docs' ? ... : activeTopMenu === 'plans' ? ... : ...} -
+
{children}
``` ### 파일 5: `src/app/main/MainSidebar.tsx` - 소개 영역과 API 메뉴 선택 타입을 공통 라우팅 구조에 맞게 정리했습니다. ```diff + introColor, + introTag, + introDescription, - const activeTagColor = isDocsGroup ? 'gold' : activeTopMenu === 'play' ? 'cyan' : 'green'; - ... + {introTag} + {introDescription} - 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(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 ( +
+ + {selectedApiMenu === 'components' ? ( + + ) : ( + + )} + +
+ ); +} ``` ### 파일 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(null); +const savedLayoutFitContentRef = useRef(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'; + + + -export { MainView } from './MainView'; +export { AppShell } from './AppShell'; +export { MainView } from './MainView'; -export function MainView() { ...legacy layout... } +export function MainView() { + return ; +} ``` ## 실행 커맨드 ```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