# 2026-04-11 작업일지

## 오늘 작업

- 게시판 글을 자동화 항목으로 접수하는 흐름을 열고, 중복 접수 상태와 권한 검사를 같이 묶어 게시판에서 바로 후속 작업으로 넘길 수 있게 정리했습니다.
- 게시판 기반 Markdown 자동 등록 자동화는 설정, 워커 스케줄, 대상 폴더 규칙을 한 흐름으로 묶고 주기 계산을 분 단위 기준으로 다시 맞췄습니다.
- Plan 반복 요청은 기존 메모 내부 옵션에서 분리해 전용 스케줄 기능으로 옮기고, 스케줄 화면과 API, 서버 저장 구조를 새로 연결했습니다.
- Plan 화면은 목록 필터, 체크리스트, 릴리즈 요약, 증적 탭, 게시판 연결, 이슈/조치 기록, 본문 최대화, 읽기 전용 제어를 순차 보강했습니다.
- Plans 진입 구조는 불필요한 중간 화면을 걷어내고 `Plan / 게시판 / 차트 / 스케줄 / 히스토리`가 직접 연결되도록 다시 정리했습니다.
- 공통 컴포넌트 묶음으로 `FormField`, `StateKit`, `DataListTable`을 추가해 이후 화면 확장에 재사용할 수 있는 기반을 확보했습니다.
- 오늘 증적용으로 `Plan/자동화` 전체 화면 1장과 상단 개요 영역 1장을 `docs/assets/worklogs/2026-04-11/`에 저장했습니다.

## 이슈 및 해결

- 게시판 초기화가 동시에 들어오면 테이블과 컬럼 생성 경합으로 첫 진입이 실패할 수 있었습니다.
- 생성 전 존재 여부를 다시 확인하는 방식으로 setup 경합을 흡수해 초기 로드 실패를 줄였습니다.
- 게시판 자동 등록은 대기 글이 없을 때 다음 주기까지 완전히 멈춰 추천 문서 Plan이 더 이상 생기지 않는 구간이 있었습니다.
- 워커가 빈 큐에서도 다음 스케줄을 유지하도록 바꾸고, 분/초 단위 혼용 계산도 함께 보정했습니다.
- Plan 반복 요청 옵션을 메모 등록 폼 안에 유지하니 일반 메모 편집과 스케줄성 작업이 한 화면에서 섞여 판단이 어려웠습니다.
- 반복 요청 UI를 걷어내고 전용 스케줄 화면과 서버 테이블로 분리해 책임 경계를 명확히 했습니다.
- Plan 상세는 자동화 접수 항목인데도 일부 상태값이나 이력 입력이 우회 수정될 수 있었습니다.
- 잠금 판정을 공통 함수로 모으고, 권한 토큰이 없는 사용자는 조치 입력과 상태 액션이 모두 읽기 전용으로만 보이게 막았습니다.
- 증적/미리보기 전체화면은 내부 스크롤 대신 부모 스크롤이 따라 움직여 읽기 흐름이 깨졌습니다.
- 전체화면 진입 시 `body/html` 스크롤을 고정하고, 모달 내부 스크롤만 유지하도록 바꿔 사용 흐름을 안정화했습니다.

## 결정 사항

- 게시판에서 만들어진 자동화 접수 건은 기본적으로 `release` 기준으로 처리하고 `main` 자동 반영은 별도 정책이 없는 한 꺼 둡니다.
- 반복성 작업은 일반 메모 옵션이 아니라 전용 스케줄 목록에서 관리합니다.
- Plan 상세에서 자동화로 접수된 원본 요청은 수정 대신 조치 기록과 증적 확인 중심으로 다룹니다.
- Plans 화면은 중간 안내 카드보다 실제 작업 화면을 바로 여는 구성이 우선입니다.
- 오늘 대표 증적 화면은 `Plan/자동화` 전체 화면, 부분 증적은 상단 개요 카드로 통일합니다.

## 상세 작업 내역

- 오전 초반에는 게시판 setup 경합과 자동화 접수 진입점을 먼저 정리해, 게시판 글이 실패 없이 열리고 필요한 경우 바로 자동화 요청으로 이어지게 만들었습니다.
- 이어서 게시판 글을 Markdown 생성 작업으로 돌리는 자동화 흐름을 붙였고, 추천 문서 생성 규칙과 스케줄 조건을 계속 수정하면서 빈 큐에서도 다음 주기를 유지하도록 다듬었습니다.
- 공통 컴포넌트 추가는 이후 화면 정리에 필요한 기반 작업으로 진행했고, 새 입력/상태/목록 컴포넌트를 샘플과 함께 공개해 재사용 가능한 토대를 마련했습니다.
- 중반 작업에서는 반복 요청 옵션을 걷어내고 전용 스케줄 기능으로 치환하는 쪽으로 방향을 바꿨습니다. 이 과정에서 저장 구조, API, 워커, 화면을 함께 움직여 반복 작업 관리의 중심을 스케줄 화면으로 옮겼습니다.
- 이후 Plans 네비게이션은 실제 사용 흐름에 맞게 다시 접었고, 게시판 복구, 차트/스케줄/히스토리 재연결, 용어 정리, 검색 라벨 통일을 거쳐 지금 구조로 수렴시켰습니다.
- 오후 후반에는 Plan 목록과 상세 화면 품질을 집중 보강했습니다. 조합 필터, 체크리스트, 릴리즈 요약, 증적 탭, 게시판 연결, 이슈 조치 기록이 순차적으로 붙었고, 본문 최대화와 전체화면 스크롤 제어도 여기서 함께 정리됐습니다.
- 마감 단계에서는 자동화 접수 건의 잠금 규칙을 다시 점검해 우회 수정 가능성을 줄였고, 비토큰 사용자 읽기 전용 제어와 알림 링크 연결까지 보완한 뒤 오늘 작업일지와 증적 캡처를 최신 상태로 정리했습니다.

## 스크린샷

![feature-plans-automation-full](../assets/worklogs/2026-04-11/feature-plans-automation-full.png)

- `Plan/자동화` 전체 화면 캡처입니다. 오늘 정리한 목록, 상세 진입 흐름, 상단 개요 구성이 한 화면에서 보이도록 전체 페이지 기준으로 저장했습니다.

![plan-board-overview](../assets/worklogs/2026-04-11/plan-board-overview.png)

- 상단 개요 카드 부분 캡처입니다. 자동 새로고침 상태, 작업 개수 요약, 안내 문구처럼 오늘 보강한 운영 문맥을 빠르게 확인할 수 있도록 따로 남겼습니다.

## 소스

### 파일 1: `etc/servers/work-server/src/routes/board.ts`, `etc/servers/work-server/src/services/board-service.ts`, `src/features/board/BoardPage.tsx`, `src/features/board/api.ts`, `src/features/board/types.ts`

- 게시판 글을 즉시 자동화 Plan으로 접수하고, 접수 중복과 권한을 함께 처리하는 흐름입니다.

```diff
+app.post('/api/board/posts/:id/actions/automation-receive', async (request, reply) => {
+  if (!requireBoardAutomationAccess(request, reply)) {
+    return;
+  }
+  const result = await receiveBoardPostAutomation(id);
+  return { ok: true, item: result.item, planItemId: result.planItemId, alreadyReceived: result.alreadyReceived };
+});
+if (currentRow.automation_received_at || currentRow.automation_plan_item_id) {
+  return { item: mapBoardPostRow(currentRow), planItemId: ..., alreadyReceived: true };
+}
+const workId = `board-post-${id}`;
+note: [`게시판 제목: ${title}`, '', content].join('\n'),
+auto_deploy_to_main: false,
+const [automationReceivingId, setAutomationReceivingId] = useState<number | null>(null);
+const [automationReceiveError, setAutomationReceiveError] = useState<string | null>(null);
+icon={<PlayCircleOutlined />}
+Tag color={automationStatus.color}
```

### 파일 2: `etc/servers/work-server/src/services/board-service.ts`, `etc/servers/work-server/src/workers/plan-worker.ts`, `etc/servers/work-server/src/services/app-config-service.ts`, `src/app/main/MainHeader.tsx`, `src/app/main/appConfig.ts`

- 게시판 Markdown 자동 등록 자동화를 설정과 워커 주기 계산까지 포함해 운영 가능한 상태로 묶었습니다.

```diff
+markdownPlanItemId: number | null;
+markdownExportedAt: string | null;
+function buildBoardPostMarkdownPlanNote(row: Record<string, unknown>, targetFolder: string) {
+  return [
+    `게시판 추천 글 #${id}을 Markdown 문서로 등록해 주세요.`,
+    `대상 파일: ${targetPath}`,
+  ].join('\n');
+}
+export async function createNextBoardPostMarkdownAutomationPlan(targetFolder: string, releaseTarget = 'release') {
+  work_id: `board-md-${id}`,
+  auto_deploy_to_main: true,
+}
+private evaluateBoardMarkdownAutomationSchedule(config, now) {
+  const scheduleType = config?.scheduleType ?? 'interval';
+  return { due, nextEligibleAt, scheduleLabel };
+}
+await this.processBoardMarkdownAutomation(appConfig);
+intervalMinutes: z.coerce.number().int().min(1).default(5)
```

### 파일 3: `src/components/formField/FormField.tsx`, `src/components/stateKit/StateKit.tsx`, `src/components/dataListTable/DataListTable.tsx`, `src/index.ts`

- 입력/상태/목록 UI를 공통 컴포넌트로 분리해 이후 Plan, Board, History 화면이 같은 토대 위에서 확장되도록 만들었습니다.

```diff
+export type DataListTableProps<T extends object> = {
+  data: T[];
+  searchFields?: ReadonlyArray<keyof T | ((item: T) => string)>;
+  filters?: ReadonlyArray<DataListFilterOption<T>>;
+  mobileCardRender?: (item: T) => ReactNode;
+};
+export function DataListTable<T extends object>({ ... }: DataListTableProps<T>) {
+  const filteredData = useMemo(() => { ... }, [...]);
+  return <Table<T> ... />;
+}
+export type FormFieldProps = Omit<FormItemProps, 'children' | 'help' | 'required'> & {
+  error?: ReactNode;
+  children: ReactNode | ((state: FormFieldRenderState) => ReactNode);
+};
+export { FormField } from './components/formField';
+export { StateKit } from './components/stateKit';
+export { DataListTable } from './components/dataListTable';
```

### 파일 4: `etc/servers/work-server/src/services/plan-service.ts`, `etc/servers/work-server/src/services/plan-schedule-service.ts`, `etc/servers/work-server/src/routes/plan.ts`, `etc/servers/work-server/src/workers/plan-worker.ts`, `src/features/planBoard/PlanSchedulePage.tsx`, `src/features/planBoard/api.ts`

- 반복 요청을 메모 옵션에서 분리하고 전용 스케줄 저장소와 화면으로 재편했습니다.

```diff
+export const PLAN_SCHEDULED_TASK_TABLE = 'plan_scheduled_tasks';
+export const createPlanScheduledTaskSchema = z.object({
+  workId: z.string().trim().optional().default('반복작업'),
+  repeatIntervalMinutes: z.coerce.number().int().min(1).max(1440).default(60),
+});
+app.get('/api/plan/scheduled-tasks', async (request, reply) => { ... });
+app.post('/api/plan/scheduled-tasks', async (request, reply) => { ... });
+app.patch('/api/plan/scheduled-tasks/:id', async (request, reply) => { ... });
+app.delete('/api/plan/scheduled-tasks/:id', async (request, reply) => { ... });
+await ensurePlanScheduledTaskTable();
+return fetchWithFallback('/api/plan/scheduled-tasks', '/api/plans/scheduled-tasks');
+'/plan/schedule', '/plan/schedules', '/plans/schedule', '/plans/schedules'
```

### 파일 5: `src/features/planBoard/PlanBoardPage.tsx`, `src/features/history/HistoryPage.tsx`, `src/features/board/BoardPage.tsx`, `src/styles.css`

- Plan 목록과 상세는 필터, 요약, 증적, 본문 최대화, 잠금 규칙, 읽기 전용 제어를 중심으로 계속 보강했습니다.

```diff
+function isPlanItemRequestLocked(item: Pick<PlanItem, 'startedAt'> | null | undefined) {
+  return Boolean(item?.startedAt);
+}
+const [noteExpanded, setNoteExpanded] = useState(false);
+const canAppendActionHistory = hasAccess && Boolean(selectedItem) && isPlanItemRequestLocked(selectedItem);
+if (!hasAccess) {
+  messageApi.error('설정 > 토큰 관리에서 권한 토큰을 등록한 사용자만 조치 이력을 추가할 수 있습니다.');
+}
+if (isPlanItemRequestLocked(currentItem)) {
+  messageApi.warning('자동화 접수된 항목은 기능동작확인을 수정할 수 없습니다.');
+}
+className={`plan-board-page__notepad-frame${noteExpanded ? ' plan-board-page__notepad-frame--expanded' : ''}`}
+className={`board-page__editor-frame${contentExpanded ? ' board-page__editor-frame--expanded' : ''}`}
+options={FUNCTION_CHECK_OPTIONS}
+disabled={!hasAccess || jangsingProcessingSavingId === item.id || isPlanItemRequestLocked(item)}
```

### 파일 6: `src/app/main/MainContent.tsx`, `src/app/main/layout/MainLayout.tsx`, `src/app/main/layout/buildSearchOptions.ts`, `src/app/main/mainView/constants.tsx`, `src/app/main/mainView/navigation.ts`, `src/app/main/mainView/searchOptions.ts`, `src/app/main/pages/PlansPage.tsx`, `src/app/main/routes.tsx`, `src/app/main/MainHeader.tsx`

- Plans 진입 구조를 단순화하고, 게시판/차트/스케줄/히스토리 복구와 용어 정리를 같은 축에서 마무리했습니다.

```diff
+type PlanSectionKey = 'all' | 'board' | 'charts' | 'schedule' | 'history' | 'release';
+navigateTo(buildPlansPath('board'));
+navigateTo(buildPlansPath('schedule'));
+navigateTo(buildPlansPath('history'));
+label: '자동화'
+label: 'Plan'
+title: '자동화'
+return <PlanBoardPage />;
+release target link -> '*.sm-home.cloud'
+search label: 'Plan'
```

### 파일 7: `etc/servers/work-server/src/services/plan-notification-service.ts`, `src/sw.js`, `src/components/previewer/PreviewerUI.tsx`, `src/components/previewer/CodexDiffPreviewer.tsx`

- 알림 클릭 이동과 전체화면 미리보기는 마지막 품질 보완으로 정리했습니다.

```diff
+function buildPlanNotificationTargetUrl(planId: number, workId: string | null | undefined, eventType: string) {
+  const baseUrl = eventType.startsWith('release-') ? 'https://rel.sm-home.cloud/' : 'https://sm-home.cloud/';
+  targetUrl.searchParams.set('planId', String(planId));
+}
+targetUrl = notificationData.targetUrl ? new URL(String(notificationData.targetUrl)) : new URL('/', self.location.origin);
+const scrollY = window.scrollY;
+document.body.style.position = 'fixed';
+document.body.style.top = `-${scrollY}px`;
+document.documentElement.style.overflow = 'hidden';
+window.scrollTo(0, scrollY);
```

### 파일 8: `docs/components/component-addition-suggestions.md`

- 게시판 논의 결과를 문서 증적으로 옮겨 오늘 제안 정리 흐름도 문서 세트 안에서 바로 확인할 수 있게 했습니다.

```diff
+## 이번 반영 요약
+- FormField: 폼 레이블, 도움말, 오류 메시지를 같은 포맷으로 묶는 공통 입력 레이어
+- StateKit: 로딩, 비어 있음, 오류 상태를 화면마다 반복 작성하지 않도록 정리한 상태 표현 묶음
+- DataListTable: 검색, 필터, 모바일 카드 대체 렌더를 함께 제공하는 공통 목록 레이어
```

## 실행 커맨드

```bash
find docs/worklogs -maxdepth 2 -type f -name '*.md' | sort
grep -RInE '작업일지|실행 커맨드|소스 탭|상세 작업 내역' docs/worklogs --include='*.md'
git -c safe.directory=/workspace/auto_codex/repo branch --show-current
git -c safe.directory=/workspace/auto_codex/repo status --short --branch
git -c safe.directory=/workspace/auto_codex/repo log --since='2026-04-11 00:00' --pretty=format:'%h %ad %d %s' --date=iso-local --reverse --all
git -c safe.directory=/workspace/auto_codex/repo log --since='2026-04-11 00:00' --name-status --pretty=format:'commit %h' --all
git -c safe.directory=/workspace/auto_codex/repo diff 6f7a9aa..a6a3c92 -- src/features/planBoard/PlanBoardPage.tsx
git -c safe.directory=/workspace/auto_codex/repo show 818cc2f -- src/features/board/BoardPage.tsx src/features/board/api.ts src/features/board/types.ts etc/servers/work-server/src/routes/board.ts etc/servers/work-server/src/services/board-service.ts
git -c safe.directory=/workspace/auto_codex/repo show d9d124b -- src/features/planBoard/PlanSchedulePage.tsx src/features/planBoard/api.ts etc/servers/work-server/src/routes/plan.ts etc/servers/work-server/src/services/plan-schedule-service.ts etc/servers/work-server/src/workers/plan-worker.ts
git -c safe.directory=/workspace/auto_codex/repo show dd2e975 -- src/components/formField/FormField.tsx src/components/stateKit/StateKit.tsx src/components/dataListTable/DataListTable.tsx src/index.ts
git -c safe.directory=/workspace/auto_codex/repo show 4e48813 -- src/app/main/MainHeader.tsx src/app/main/appConfig.ts etc/servers/work-server/src/workers/plan-worker.ts etc/servers/work-server/src/services/board-service.ts
npm run build:app
PORT=4173 node scripts/serve-app-dist.mjs
curl -I http://127.0.0.1:4173/plans/all
node --input-type=module <<'EOF'
// Playwright로 /plans/all 진입 후 전체 화면과 개요 카드 캡처 저장
EOF
find docs/assets/worklogs/2026-04-11 -maxdepth 1 -type f | sort
```

## 변경/신규 파일

- A docs/worklogs/2026-04-11.md
- A docs/assets/worklogs/2026-04-11/feature-plans-automation-full.png
- A docs/assets/worklogs/2026-04-11/plan-board-overview.png
- M docs/components/component-addition-suggestions.md
- M etc/db/work-db/sql/board-posts.sql
- M etc/servers/work-server/src/routes/board.ts
- M etc/servers/work-server/src/routes/plan.ts
- M etc/servers/work-server/src/routes/visitor-history.ts
- M etc/servers/work-server/src/services/app-config-service.ts
- M etc/servers/work-server/src/services/board-service.ts
- M etc/servers/work-server/src/services/plan-notification-service.ts
- A etc/servers/work-server/src/services/plan-schedule-service.ts
- M etc/servers/work-server/src/services/plan-service.ts
- M etc/servers/work-server/src/services/visitor-history-service.ts
- M etc/servers/work-server/src/services/worklog-automation-service.ts
- M etc/servers/work-server/src/services/worklog-automation-utils.ts
- A etc/servers/work-server/src/workers/plan-worker.test.ts
- M etc/servers/work-server/src/workers/plan-worker.ts
- M src/app/main/MainContent.tsx
- M src/app/main/MainHeader.tsx
- M src/app/main/appConfig.ts
- M src/app/main/clientIdentity.ts
- M src/app/main/layout/MainLayout.tsx
- M src/app/main/layout/buildSearchOptions.ts
- M src/app/main/mainView/constants.tsx
- M src/app/main/mainView/navigation.ts
- M src/app/main/mainView/searchOptions.ts
- M src/app/main/pages/PlansPage.tsx
- M src/app/main/routes.tsx
- A src/components/dataListTable/DataListTable.css
- A src/components/dataListTable/DataListTable.tsx
- A src/components/dataListTable/index.ts
- A src/components/dataListTable/samples/BaseSample.tsx
- A src/components/formField/FormField.css
- A src/components/formField/FormField.tsx
- A src/components/formField/index.ts
- A src/components/formField/samples/BaseSample.tsx
- M src/components/previewer/CodexDiffPreviewer.tsx
- M src/components/previewer/PreviewerUI.tsx
- A src/components/stateKit/StateKit.css
- A src/components/stateKit/StateKit.tsx
- A src/components/stateKit/index.ts
- A src/components/stateKit/samples/BaseSample.tsx
- M src/features/board/BoardPage.tsx
- M src/features/board/api.ts
- M src/features/board/types.ts
- M src/features/history/HistoryPage.tsx
- M src/features/history/api.ts
- M src/features/planBoard/PlanBoardPage.tsx
- A src/features/planBoard/PlanSchedulePage.tsx
- M src/features/planBoard/api.ts
- M src/features/planBoard/index.ts
- M src/features/planBoard/types.ts
- M src/index.ts
- M src/store/appStore/context/AppStoreContext.tsx
- M src/styles.css
- M src/sw.js
