# 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(null); +const [automationReceiveError, setAutomationReceiveError] = useState(null); +icon={} +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, 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 = { + data: T[]; + searchFields?: ReadonlyArray string)>; + filters?: ReadonlyArray>; + mobileCardRender?: (item: T) => ReactNode; +}; +export function DataListTable({ ... }: DataListTableProps) { + const filteredData = useMemo(() => { ... }, [...]); + return ... />; +} +export type FormFieldProps = Omit & { + 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 | 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 ; +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