# 2026-04-09 작업일지 ## 오늘 작업 - `Play > Layout`을 컴포넌트/위젯 선택형 레이아웃 편집기로 확장하고, 저장 레이아웃 목록/상세 흐름을 여러 차례 다듬었습니다. - 레이아웃 저장소를 브라우저 `IndexedDB`에서 `work-db` API 기반 서버 저장 방식으로 전환했습니다. - `Window UI` 검색 중복 열림, 모바일 리사이즈 hit area, 더블탭/더블클릭 확장, 스크롤 처리 문제를 순차적으로 정리했습니다. - `EmbeddedMapUI`, `GPS Sample Widget`, 서버 경유 푸시 알림 흐름을 추가해 GPS 거점/반경/알림 시나리오를 앱 안에서 직접 다룰 수 있게 만들었습니다. - `Text Memo Widget`을 신규 추가하고, 이어서 iOS 메모 느낌의 시트형 UI로 재구성했습니다. - `Plan Board`에 로컬 페이지네이션, `release 반영 상태`/`자동화 실패` 바로가기, `main` 일괄 반영 흐름을 추가했습니다. - 앱 초기 1.9초 로딩 오버레이, 제스처 단축키 설정, `release 반영 상태` 메뉴/모달, Git flow 안전 규칙 문서 보강까지 반영했습니다. - 오늘자 작업 증적을 위해 전체 화면 1장과 위젯 단위 부분 캡처 1장을 `docs/assets/worklogs/2026-04-09/`에 저장하고 연결했습니다. ## 이슈 및 해결 - `Play > Layout` 저장본을 클라이언트 로컬 저장소만으로 관리하던 상태라 작업 환경이 바뀌면 레이아웃이 끊겼습니다. - `src/views/play/layoutStorage.ts`를 `work-db` API 호출 구조로 전환하고, 인증 토큰/클라이언트 ID 헤더를 함께 보내도록 바꿔 저장본을 서버 기준으로 유지했습니다. - `Window UI` 검색/열기 흐름에서 같은 항목이 짧은 시간 안에 2개씩 열리거나, 전체 부모 기준으로 과도하게 확장되는 재발 이슈가 있었습니다. - `SearchLayerContext`, `SearchCommandModal`, `WindowUI`를 함께 수정해 선택 잠금, 500ms 중복 차단, 보이는 viewport 기준 확장 로직으로 안정화했습니다. - 지도 반경 오버레이는 초기에 좌표 기반 렌더가 아니라 CSS 고정 오버레이 성격이 강해 지도를 움직일 때 의미가 깨졌습니다. - `EmbeddedMapUI`를 실제 좌표/경계 계산 기반 OSM iframe 오버레이로 바꾸고, GPS 위젯에서 선택 거점과 현재 위치를 함께 표시하도록 맞췄습니다. - 자동화 쪽은 `main` 반영이 단건 기준으로만 움직여 `release`에 쌓인 항목을 한 번에 넘기기 어려웠습니다. - `plan-service.ts`, `plan-worker.ts`에서 `release_target` 기준 `main` 일괄 반영/재시도 흐름으로 보강했습니다. ## 결정 사항 - `Play > Layout`의 대표 명칭은 `Layout Editor`로 통일하고, 저장된 레이아웃은 별도 메뉴 엔트리로 노출합니다. - 레이아웃/플레이그라운드 저장은 브라우저 로컬 상태보다 `work-db` 서버 저장을 우선 기준으로 사용합니다. - `Plan` 상단/설정 영역의 `release 반영 상태`, `자동화 실패`는 별도 빠른 진입점으로 유지합니다. - `hotfix`와 `feature` 후속 작업은 항상 새 브랜치에서 다시 시작한다는 Git 안전 규칙을 문서에 명시합니다. - 오늘 대표 스크린샷은 `Play > Layout`, 부분 스크린샷은 실제 위젯 단위로 확인 가능한 `GPS Sample Widget` 카드로 남깁니다. ## 상세 작업 내역 - `src/views/play/LayoutPlaygroundView.tsx`에서 섹션별 `showHideAction`, 컴포넌트/위젯 검색 모달, preview-only 렌더, 저장 레이아웃 목록/상세, 전체보기/빈 상태 플로우를 누적 확장했습니다. - `src/views/play/layoutStorage.ts`에서 `play_layouts` 테이블을 다루는 서버 API 저장소로 전환하고, fallback URL과 timeout 처리까지 추가했습니다. - `src/components/search/SearchCommandModal.tsx`, `src/layer/search/context/SearchLayerContext.tsx`, `src/components/window/WindowUI.tsx`, `src/components/window/WindowUI.css`에서 검색 선택 중복, 모바일 hit area, 보이는 화면 기준 확장, 닫기 액션을 정리했습니다. - `src/components/embeddedMap/*`, `src/layer/gps/*`, `src/widgets/gps-sample-card/*`, `etc/servers/work-server/src/routes/notification.ts`, `src/app/main/notificationApi.ts`에서 지도 내장 UI, GPS 레이어, 웹푸시 알림 경로를 구축했습니다. - `src/widgets/text-memo-widget/*`, `src/widgets/core/WidgetShell.tsx`, `src/widgets/registry.ts`, `src/index.ts`에서 메모 위젯을 추가하고 위젯 카드 래퍼 옵션과 샘플 연결을 마쳤습니다. - `src/features/planBoard/PlanBoardPage.tsx`, `src/features/planBoard/quickFilters.ts`, `src/app/main/MainHeader.tsx`, `src/app/main/MainView.tsx`, `src/app/main/MainContent.tsx`에서 plan 빠른 필터/로컬 패이징/윈도우 열기/메뉴 정리를 반영했습니다. - `etc/servers/work-server/src/services/plan-service.ts`, `etc/servers/work-server/src/workers/plan-worker.ts`에서 `main` 일괄 반영 요청과 배치 처리 로직을 추가했습니다. - `src/App.tsx`, `src/styles.css`에 앱 시작 로딩 오버레이와 로그 스트림 애니메이션을 넣었습니다. - `AGENTS.md`에 `release` 반영 직후 같은 브랜치에서 작업을 이어가지 않도록 하는 안전 체크리스트와 꼬임 위험 신호를 추가했습니다. - 오늘 업무일지 문서와 스크린샷 자산은 현재 날짜 기준 Git 이력과 실제 캡처 결과로 다시 정리했습니다. ## 스크린샷 ![feature-play-layout](../assets/worklogs/2026-04-09/feature-play-layout.png) - `Play > Layout` 전체 화면입니다. 저장 레이아웃/편집/분할/섹션 선택 흐름을 한 번에 확인할 수 있습니다. ![widget-gps-sample](../assets/worklogs/2026-04-09/widget-gps-sample.png) - `GPS Sample Widget` 부분 캡처입니다. 오늘 추가된 지도/거점/반경 위젯 단위 증적입니다. ## 소스 ### 파일 1: `src/views/play/LayoutPlaygroundView.tsx` - `Layout Editor`의 핵심 화면으로, 컴포넌트/위젯 바인딩과 저장 레이아웃 상세 플로우를 대부분 이 파일에서 확장했습니다. ```diff +import { SearchCommandModal, type SearchKeywordOption } from '../../components/search'; +import { componentSampleEntries, widgetSampleEntries } from '../../app/manifests/samples.manifest'; +type LayoutComponentBinding = { + optionId: string; + label: string; + description?: string; + keywords: string[]; +}; +type LayoutPlaygroundViewProps = { + savedLayoutViewId?: string | null; + showSavedLayoutsOnly?: boolean; + onSavedLayoutsChange?: (layouts: SavedLayoutRecord[]) => void; +}; - title: string; - description: string; + showHideAction: boolean; + componentBinding: LayoutComponentBinding | null; ``` ### 파일 2: `src/views/play/layoutStorage.ts` - 레이아웃 저장소를 `IndexedDB`에서 `work-db` API 기반 서버 저장 방식으로 전환했습니다. ```diff +import { appendClientIdHeader } from '../../app/main/clientIdentity'; +import { getRegisteredAccessToken } from '../../app/main/tokenAccess'; -const DATABASE_NAME = 'play-layout-db'; -const STORE_NAME = 'saved-layouts'; -const DATABASE_VERSION = 1; +const WORK_SERVER_TIMEOUT_MS = 8000; +const PLAY_LAYOUTS_TABLE = 'play_layouts'; +class LayoutStorageError extends Error { + status: number; +} ``` ### 파일 3: `src/components/search/SearchCommandModal.tsx` - 검색 모달 제목/설명 커스터마이징, 모바일 축약 문구, 선택 잠금 로직을 추가했습니다. ```diff + title?: string; + description?: string; + placeholder?: string; + submitHint?: string; + const selectionLockRef = useRef(false); + const [isMobileViewport, setIsMobileViewport] = useState(() => { + return window.innerWidth <= 768; + }); + const submitOption = (option: SearchKeywordOption | undefined) => { + if (!option || selectionLockRef.current) { + return; + } + selectionLockRef.current = true; + onSelectOption(option); + onClose(); + }; ``` ### 파일 4: `src/components/window/WindowUI.tsx` - `Window UI`는 닫기 액션, 모바일 리사이즈 개선, 보이는 viewport 기준 더블탭/더블클릭 확장 로직을 받았습니다. ```diff -import { FullscreenExitOutlined, FullscreenOutlined, MinusOutlined } from '@ant-design/icons'; +import { CloseOutlined, FullscreenExitOutlined, FullscreenOutlined, MinusOutlined } from '@ant-design/icons'; +type ResizeTapRecord = { + direction: ResizeDirection; + at: number; + x: number; + y: number; +}; +const DOUBLE_TAP_DELAY = 320; +const DOUBLE_TAP_MOVE_TOLERANCE = 18; +const RESIZE_MOVE_THRESHOLD = 4; +function getVisibleParentBounds(element: HTMLDivElement | null) { + const viewportWidth = window.visualViewport?.width ?? window.innerWidth; +} ``` ### 파일 5: `src/components/embeddedMap/EmbeddedMapUI.tsx` - 새 내장 지도 UI를 추가하고, 좌표/반경/보조 마커를 OSM iframe 위에 오버레이로 표시하도록 구성했습니다. ```diff +export type EmbeddedMapUIProps = { + latitude: number; + longitude: number; + radiusMeters?: number; + lockViewport?: boolean; + secondaryMarker?: { + latitude: number; + longitude: number; + label?: string; + } | null; + overlay?: ReactNode; +}; +function createEmbedUrl(bounds: Bounds, latitude: number, longitude: number) { + return `https://www.openstreetmap.org/export/embed.html?...`; +} ``` ### 파일 6: `src/widgets/gps-sample-card/GpsSampleWidget.tsx` - GPS on/off, 현재 좌표, 거점 저장, 반경, In/Out 푸시 알림, 지도 표시를 한 위젯으로 묶었습니다. ```diff +import { + AimOutlined, + BellOutlined, + EnvironmentOutlined, + DeleteOutlined, + RadarChartOutlined, +} from '@ant-design/icons'; +import { EmbeddedMapUI } from '../../components/embeddedMap'; +import { useGpsLayer } from '../../layer'; +const selectedAnchor = anchors.find((anchor) => anchor.id === selectedAnchorId) ?? null; +const distanceToSelectedAnchor = + selectedAnchor && currentPosition + ? calculateDistanceMeters(...) + : null; ``` ### 파일 7: `src/widgets/text-memo-widget/TextMemoWidget.tsx` - `Text Memo Widget`을 신규 추가하고 최근 6개 메모 저장/불러오기/삭제와 시트형 편집 UI를 구성했습니다. ```diff +const STORAGE_KEY = 'ai-code-app:text-memo-widget'; +const MAX_SAVED_NOTES = 6; +type SavedNote = { + id: string; + body: string; + createdAt: string; +}; +export const TextMemoWidget = forwardRef(function TextMemoWidget( + { cardWrapper }, + ref, +) { ``` ### 파일 8: `src/features/planBoard/PlanBoardPage.tsx` - plan 목록 로컬 패이징, 빠른 필터, 최대화 뒤 아이콘-only 돌아가기, 긴 본문 접힘 흐름을 오늘 기준으로 누적 반영했습니다. ```diff + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 10; + const pagedItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize); + useEffect(() => { + setCurrentPage(1); + }, [filter, searchQuery]); - label: '요약', + label: '작업란', ``` ### 파일 9: `src/app/main/MainHeader.tsx` - 설정 메뉴에 `release 상태 작업`, `자동화 실패` 바로가기를 추가하고 현재 건수를 같이 보여주도록 확장했습니다. ```diff +import { isAutomationFailedItem, isReleasePendingMainItem } from '../../features/planBoard/quickFilters'; +const [planShortcutCounts, setPlanShortcutCounts] = useState({ + releasePendingMain: 0, + automationFailed: 0, +}); +void fetchPlanItems('all') + .then((items) => { + setPlanShortcutCounts({ + releasePendingMain: items.filter(isReleasePendingMainItem).length, + automationFailed: items.filter(isAutomationFailedItem).length, + }); + }) ``` ### 파일 10: `src/app/main/MainView.tsx` - 상단/사이드 메뉴 구조를 `release 반영 상태`, `APIs / Widgets`, `Layout Editor`, 저장 레이아웃 메뉴까지 포괄하도록 재정리했습니다. ```diff - 'in-progress': '작업중', + 'in-progress': '자동화 대기 / 작업 중', - error: '오류', + error: '오류 (작업 완료 전)', + release: 'release 반영 상태', +const PLAY_LAYOUT_RECORD_PREFIX = 'layout-record:' as const; +function resolvePlanQuickFilterMenu(filter: PlanQuickFilter): Extract { + return filter === 'release-pending-main' ? 'release' : 'error'; +} ``` ### 파일 11: `src/app/main/MainContent.tsx` - 검색 결과를 `Window UI` 창으로 여는 흐름, 샘플 숨김 목록, `Plan`/저장 레이아웃 렌더 분기를 추가했습니다. ```diff +import { useMemo, useState, type ReactNode } from 'react'; +import { WindowUI, type WindowFrame } from '../../components/window'; +import { useSearchLayer } from '../../layer'; +const HIDDEN_COMPONENT_IDS = ['search-command-modal', 'window-ui']; +const { windowSelections, clearWindowSelection } = useSearchLayer(); +const [windowFrames, setWindowFrames] = useState>({}); +const [windowZIndexes, setWindowZIndexes] = useState>({}); ``` ### 파일 12: `src/App.tsx` - 앱 시작 직후 1.9초 동안 보여주는 풀스크린 로딩 오버레이와 로그 스트림 UI를 추가했습니다. ```diff +const INITIAL_LOADING_LOGS = [ + 'BOOT SEQUENCE :: app shell warmup', + 'CONFIG SYNC :: workspace profile applied', + 'SESSION LINK :: reconnecting realtime channel', + 'MODULE CHECK :: dashboard widgets online', + 'READY SIGNAL :: rendering main viewport', +]; +const [showInitialLoading, setShowInitialLoading] = useState(true); +window.setTimeout(() => { + setShowInitialLoading(false); +}, 1900); ``` ### 파일 13: `etc/servers/work-server/src/services/plan-service.ts` - `main` 반영 요청/클레임/완료 처리 모두를 `release_target` 기준 배치 처리로 바꿨습니다. ```diff - isMainRetry ? 'main 반영 재시도를 요청했습니다.' : 'release 반영 재시도를 요청했습니다.', + isMainRetry ? 'main 일괄 반영 재시도를 요청했습니다.' : 'release 반영 재시도를 요청했습니다.', + const pendingRows = await db(PLAN_TABLE) + .select('id') + .where({ status: '릴리즈완료', release_target: releaseTarget }); - .where({ id }) + .whereIn('id', targetIds.length > 0 ? targetIds : [id]) + message: `${releaseTarget} 브랜치 기준으로 ${Math.max(targetIds.length, 1)}건 main 일괄 반영을 요청했습니다.`, ``` ### 파일 14: `AGENTS.md` - Git 안전 규칙을 보강해 `release` 반영 뒤 같은 브랜치에서 추가 작업을 이어가는 패턴을 금지하고 점검 절차를 명시했습니다. ```diff +* `release` 반영 상태에서 현재 브랜치 그대로 추가 작업 진행 +* `release` 반영이 끝난 뒤 추가 수정 요청이 들어오면, 반드시 새 브랜치에서 다시 시작한다 +## 안전 점검 체크리스트 +1. 지금 반영할 변경이 `release`에 먼저 들어갔는지 확인 +2. 추가 요청이라면 새 `feature/*` 또는 `hotfix/*` 브랜치에서 작업 중인지 확인 +## 꼬임 위험 신호 +* `release` 반영 직후 추가 요청을 현재 브랜치에서 바로 이어서 처리하는 경우 ``` ### 파일 15: `docs/assets/worklogs/2026-04-09/feature-play-layout.png` - 오늘 대표 전체 화면 스크린샷입니다. ```diff Binary file added: docs/assets/worklogs/2026-04-09/feature-play-layout.png ``` ### 파일 16: `docs/assets/worklogs/2026-04-09/widget-gps-sample.png` - 오늘 위젯 단위 부분 스크린샷입니다. ```diff Binary file added: docs/assets/worklogs/2026-04-09/widget-gps-sample.png ``` ## 실행 커맨드 ```bash git -c safe.directory=/workspace/auto_codex/repo log --since='2026-04-09 00:00' --until='2026-04-09 23:59:59' --stat --oneline --decorate git -c safe.directory=/workspace/auto_codex/repo log --since='2026-04-09 00:00' --until='2026-04-09 23:59:59' --name-status --format='commit %H%nAuthor: %an%nDate: %ad%nSubject: %s%n' --date=iso git -c safe.directory=/workspace/auto_codex/repo diff --stat b70ce0c2549542703967b73aedf8d249050a40f7..HEAD npm run dev -- --host 127.0.0.1 --port 4173 CAPTURE_BASE_URL=http://127.0.0.1:4173 node scripts/capture-feature-screenshot.mjs play-layout 2026-04-09 node --input-type=module # GPS Sample Widget 부분 캡처 npm run build:app ``` ## 변경/신규 파일 (전체, 중복 제거, Git 기록 기준 + 오늘 업무일지 산출물) - A src/components/dashboard/multiProgress/samples/BaseSample.tsx - A src/components/dashboard/progress/samples/BaseSample.tsx - A src/components/embeddedMap/EmbeddedMapUI.css - A src/components/embeddedMap/EmbeddedMapUI.tsx - A src/components/embeddedMap/index.ts - A src/components/embeddedMap/samples/BaseSample.tsx - A src/components/embeddedMap/samples/Sample.tsx - A src/components/inputs/checkCombo/samples/BaseSample.tsx - A src/components/inputs/composite/multiInput/samples/BaseSample.tsx - A src/components/inputs/popup/samples/BaseSample.tsx - A src/components/inputs/primitives/input/samples/BaseSample.tsx - A src/components/inputs/select/samples/BaseSample.tsx - A src/components/inputs/specialized/buttonEditableInput/samples/BaseSample.tsx - A src/components/inputs/specialized/emailInput/samples/BaseSample.tsx - A src/components/markdownPreview/samples/MarkdownPreviewCardBaseSample.tsx - A src/components/markdownPreview/samples/MarkdownPreviewContentBaseSample.tsx - A src/components/markdownPreview/samples/MarkdownPreviewListBaseSample.tsx - A src/components/navigation/samples/FolderTreeNavBaseSample.tsx - A src/components/navigation/samples/SectionMenuLayoutBaseSample.tsx - A src/components/previewer/samples/BaseSample.tsx - A src/components/previewer/samples/CodexDiffBaseSample.tsx - A src/components/search/samples/BaseSample.tsx - A src/components/status-badge/samples/BaseSample.tsx - A src/components/window/samples/BaseSample.tsx - A src/features/planBoard/quickFilters.ts - A src/layer/gps/context/GpsLayerContext.tsx - A src/layer/gps/hooks/useGpsLayer.ts - A src/layer/gps/index.ts - A src/layer/gps/types/index.ts - A src/widgets/gps-sample-card/GpsSampleWidget.css - A src/widgets/gps-sample-card/GpsSampleWidget.tsx - A src/widgets/gps-sample-card/index.ts - A src/widgets/gps-sample-card/samples/Sample.tsx - A src/widgets/text-memo-widget/TextMemoWidget.css - A src/widgets/text-memo-widget/TextMemoWidget.tsx - A src/widgets/text-memo-widget/index.ts - A src/widgets/text-memo-widget/samples/Sample.tsx - M AGENTS.md - M docs/components/window-ui.md - M etc/servers/work-server/src/routes/notification.ts - M etc/servers/work-server/src/services/plan-service.ts - M etc/servers/work-server/src/workers/plan-worker.ts - M package-lock.json - M package.json - M src/App.tsx - M src/app/main/MainContent.tsx - M src/app/main/MainHeader.tsx - M src/app/main/MainView.tsx - M src/app/main/ReleasePendingMainModal.tsx - M src/app/main/appConfig.ts - M src/app/main/notificationApi.ts - M src/app/main/types.ts - M src/components/dashboard/multiProgress/samples/Sample.tsx - M src/components/dashboard/progress/samples/Sample.tsx - M src/components/embeddedMap/EmbeddedMapUI.css - M src/components/embeddedMap/EmbeddedMapUI.tsx - M src/components/embeddedMap/samples/Sample.tsx - M src/components/inputs/checkCombo/samples/Sample.tsx - M src/components/inputs/composite/multiInput/samples/Sample.tsx - M src/components/inputs/popup/samples/Sample.tsx - M src/components/inputs/primitives/input/samples/Sample.tsx - M src/components/inputs/select/samples/Sample.tsx - M src/components/inputs/specialized/buttonEditableInput/samples/Sample.tsx - M src/components/inputs/specialized/emailInput/samples/Sample.tsx - M src/components/previewer/samples/CodexDiffSample.tsx - M src/components/previewer/samples/Sample.tsx - M src/components/search/SearchCommandModal.tsx - M src/components/status-badge/samples/Sample.tsx - M src/components/window/WindowUI.css - M src/components/window/WindowUI.tsx - M src/components/window/samples/Sample.tsx - M src/components/window/types/window.ts - M src/features/layout/component-sample-gallery/ComponentSamplesLayout.tsx - M src/features/planBoard/PlanBoardPage.tsx - M src/features/planBoard/index.ts - M src/index.ts - M src/layer/gps/context/GpsLayerContext.tsx - M src/layer/index.ts - M src/layer/search/context/SearchLayerContext.tsx - M src/layer/search/types/index.ts - M src/main.tsx - M src/styles.css - M src/views/play/LayoutPlaygroundView.tsx - M src/views/play/layoutStorage.ts - M src/widgets/api-sample-card/ApiSampleCardWidget.tsx - M src/widgets/api-sample-card/samples/Sample.tsx - M src/widgets/core/WidgetShell.tsx - M src/widgets/core/index.ts - M src/widgets/core/types/widget.ts - M src/widgets/dashboard-report-card/DashboardReportCardWidget.tsx - M src/widgets/dashboard-report-card/samples/TmsDeliveryFlowSample.tsx - M src/widgets/dashboard-report-card/samples/TmsDeliveryMetricsSample.tsx - M src/widgets/dashboard-report-card/samples/WmsInboundOutboundSample.tsx - M src/widgets/dashboard-report-card/samples/WmsInventoryTrendSample.tsx - M src/widgets/gps-sample-card/GpsSampleWidget.tsx - M src/widgets/gps-sample-card/samples/Sample.tsx - M src/widgets/registry.ts - M src/widgets/text-memo-widget/TextMemoWidget.css - M src/widgets/text-memo-widget/TextMemoWidget.tsx - A docs/worklogs/2026-04-09.md - A docs/assets/worklogs/2026-04-09/feature-play-layout.png - A docs/assets/worklogs/2026-04-09/widget-gps-sample.png