# 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<WidgetHandle, TextMemoWidgetProps>(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<PlanSidebarKey, 'release' | 'error'> {
+  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<Record<string, WindowFrame>>({});
+const [windowZIndexes, setWindowZIndexes] = useState<Record<string, number>>({});
```

### 파일 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
