chore: exclude local resource artifacts from main sync

This commit is contained in:
2026-05-15 10:16:45 +09:00
parent 442879313f
commit d38d022872
504 changed files with 17074 additions and 3642 deletions

0
src/features/layout/README.md Executable file → Normal file
View File

0
src/features/layout/component-sample-gallery/index.ts Executable file → Normal file
View File

0
src/features/layout/dashboard-feature-gallery/index.ts Executable file → Normal file
View File

0
src/features/layout/dashboard-report-gallery/index.ts Executable file → Normal file
View File

0
src/features/layout/docs-markdown-preview/index.ts Executable file → Normal file
View File

0
src/features/layout/feature-markdown-preview/index.ts Executable file → Normal file
View File

View File

@@ -4,15 +4,9 @@ import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { canUseChatType, resolveCurrentChatPermissionRoles, useChatTypeRegistry } from '../../../app/main/chatTypeAccess';
import { createChatConversationRoom, fetchChatConversations } from '../../../app/main/mainChatPanel';
import {
LAYOUT_EDITOR_GUIDED_CHAT_TYPE_DESCRIPTION,
LAYOUT_EDITOR_GUIDED_CHAT_TYPE_ID,
LAYOUT_EDITOR_GUIDED_CHAT_TYPE_NAME,
} from '../../../app/main/chatTypeDefaults';
import { buildChatPath } from '../../../app/main/routes';
import { useTokenAccess } from '../../../app/main/tokenAccess';
import { SelectUI, type SelectOptionItem } from '../../../components/inputs/select';
import { resolvePreferredLayoutCodexChatType } from '../../../views/play/layoutCodexChatType';
import { listSavedLayouts, saveLayout, type SavedLayoutRecord } from '../../../views/play/layoutStorage';
import { isReusableLayoutConversation, resolveLayoutCodexRequestSocketUrl, resolveLayoutConversationTitle } from './featureMenu.chat';
import type { FeatureMenuTabKey, LayoutInteractionRule } from './featureMenu.types';
@@ -40,21 +34,17 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
const [draftBody, setDraftBody] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [isSending, setIsSending] = useState(false);
const [selectedChatTypeId, setSelectedChatTypeId] = useState<string | null>(null);
const chatPermissionRoles = useMemo(() => resolveCurrentChatPermissionRoles(hasAccess), [hasAccess]);
const availableChatTypes = useMemo(
() => chatTypes.filter((item) => canUseChatType(item, chatPermissionRoles)),
[chatPermissionRoles, chatTypes],
);
const preferredCodexChatType = useMemo(
() => resolvePreferredLayoutCodexChatType(availableChatTypes),
[availableChatTypes],
const selectedChatType = useMemo(
() => availableChatTypes.find((item) => item.id === selectedChatTypeId) ?? availableChatTypes[0] ?? null,
[availableChatTypes, selectedChatTypeId],
);
const targetChatType = preferredCodexChatType ?? {
id: LAYOUT_EDITOR_GUIDED_CHAT_TYPE_ID,
name: LAYOUT_EDITOR_GUIDED_CHAT_TYPE_NAME,
description: LAYOUT_EDITOR_GUIDED_CHAT_TYPE_DESCRIPTION,
};
const layoutOptions = useMemo<SelectOptionItem[]>(
() =>
@@ -91,6 +81,19 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
setSelectedLayoutId(fallback?.id ?? null);
}, [layoutId, savedLayouts, selectedLayoutId]);
useEffect(() => {
if (availableChatTypes.length === 0) {
setSelectedChatTypeId(null);
return;
}
if (selectedChatTypeId && availableChatTypes.some((item) => item.id === selectedChatTypeId)) {
return;
}
setSelectedChatTypeId(availableChatTypes[0]?.id ?? null);
}, [availableChatTypes, selectedChatTypeId]);
const selectedLayout = selectedLayoutId ? savedLayouts.find((item) => item.id === selectedLayoutId) ?? null : null;
const selectedLayoutInteractions = useMemo<LayoutInteractionRule[]>(
() => normalizeLayoutInteractions(selectedLayout?.tree ?? null),
@@ -276,7 +279,10 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
};
const handleCodexExecute = async () => {
if (!selectedLayout) {
if (!selectedLayout || !selectedChatType) {
if (!selectedChatType) {
void messageApi.error('사용 가능한 채팅유형이 없어 Codex 실행을 보낼 수 없습니다.');
}
return;
}
@@ -289,7 +295,7 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
try {
const conversations = await fetchChatConversations();
const matchedConversation = conversations.find((item) =>
isReusableLayoutConversation(item, conversationTitle, targetChatType.id),
isReusableLayoutConversation(item, conversationTitle, selectedChatType.id),
);
const shouldCreateConversation = !matchedConversation;
const targetSessionId =
@@ -302,10 +308,10 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
await createChatConversationRoom({
sessionId: targetSessionId,
title: conversationTitle,
chatTypeId: targetChatType.id,
lastChatTypeId: targetChatType.id,
contextLabel: targetChatType.name,
contextDescription: targetChatType.description,
chatTypeId: selectedChatType.id,
lastChatTypeId: selectedChatType.id,
contextLabel: selectedChatType.name,
contextDescription: selectedChatType.description,
notifyOffline: true,
});
}
@@ -326,9 +332,9 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
type: 'message:send',
payload: {
text: prompt,
chatTypeId: targetChatType.id,
chatTypeLabel: targetChatType.name,
chatTypeDescription: targetChatType.description,
chatTypeId: selectedChatType.id,
chatTypeLabel: selectedChatType.name,
chatTypeDescription: selectedChatType.description,
requestId,
mode: 'queue',
},
@@ -434,13 +440,29 @@ export function FeatureMenuLayoutPage({ layoutId, savedLayouts, onSavedLayoutsCh
}}
/>
</div>
<div className="feature-menu-layout-page__field">
<Text className="feature-menu-layout-page__field-label">Codex </Text>
<SelectUI
data={availableChatTypes.map((item) => ({
code: item.id,
value: item.name,
}))}
value={selectedChatType?.id ?? undefined}
allowClear={false}
disabled={availableChatTypes.length === 0}
className="feature-menu-layout-page__select"
onChange={(nextCode) => {
setSelectedChatTypeId(nextCode ?? null);
}}
/>
</div>
<Tooltip title="Codex 실행">
<Button
type="primary"
size="large"
icon={<PlayCircleOutlined />}
className="feature-menu-layout-page__run-button"
disabled={!selectedLayout}
disabled={!selectedLayout || !selectedChatType}
loading={isSending}
aria-label="Codex 실행"
onClick={() => {

View File

@@ -14,7 +14,7 @@ export function resolveLayoutCodexRequestSocketUrl(sessionId: string) {
if (['127.0.0.1', 'localhost', '0.0.0.0'].includes(window.location.hostname)) {
resolvedUrl.protocol = 'wss:';
resolvedUrl.hostname = 'test.sm-home.cloud';
resolvedUrl.hostname = 'preview.sm-home.cloud';
resolvedUrl.port = '';
resolvedUrl.pathname = '/ws/chat';
}

View File

@@ -7,8 +7,9 @@
## 작업 대상
- 패키지 루트: `src/features/layout/feature-menu/`
- 관련 저장 레이아웃 ID: `layout-1777643627048`
- 검증 기준 도메인: `https://test.sm-home.cloud/`
- 최종 확인 기준: `4173 preview`
- 운영 비교 도메인: `https://test.sm-home.cloud/`
- 소스 변경 검증 도메인: `https://preview.sm-home.cloud/`
- 로컬 preview 컨테이너: `http://127.0.0.1:4173`
## 확인된 기존 문제
- 상단 description 요약이 모바일 세로 공간을 과도하게 차지했다.

View File

@@ -61,7 +61,8 @@
## 검증 기준
- 실제 수정본이 있으면 문서 설명보다 화면 결과와 preview 검증을 우선한다.
- 검증 대상 기본 도메인은 `https://test.sm-home.cloud/`다.
- 운영 비교 도메인은 `https://test.sm-home.cloud/`다.
- 소스 변경 검증과 최종 확인 도메인은 `https://preview.sm-home.cloud/`다.
- 최종 검증 산출물은 `resources/verification/` 아래에 패키지 기준으로 함께 보관한다.
## 패키지 내부 산출물
@@ -69,4 +70,4 @@
- 개발 완료 문서: `resources/feature-menu-implementation.md`
- 최종 preview 검증 이미지: `resources/verification/feature-menu-preview-mobile-final.png`
- 최종 preview 검증 이미지: `resources/verification/feature-menu-preview-desktop-final.png`
- `test.sm-home.cloud` 비교 검증 이미지: `resources/verification/feature-menu-test-sm-home-mobile.png`
- `test.sm-home.cloud` 운영 비교 이미지: `resources/verification/feature-menu-test-sm-home-mobile.png`

View File

@@ -33,26 +33,26 @@
- 최종 분석 문서: `resources/feature-menu-analysis.md`
- 최종 preview 모바일: `resources/verification/feature-menu-preview-mobile-final.png`
- 최종 preview 데스크톱: `resources/verification/feature-menu-preview-desktop-final.png`
- `test.sm-home.cloud` 재현 확인 이미지: `resources/verification/feature-menu-test-sm-home-mobile.png`
- `test.sm-home.cloud` 운영 비교 이미지: `resources/verification/feature-menu-test-sm-home-mobile.png`
## 검증 결과
- `test.sm-home.cloud` 모바일 재현에서는 textarea 하단이 편집 쉘 아래로 약 `91px` 넘치는 기존 상태를 다시 확인했다.
- `2026-05-03` `4173 preview` 모바일 재검증에서는 `tabs.bottom = 651`, `textarea.bottom = 651`로 맞춰졌고, 마지막 줄까지 내부 스크롤로 확인됐다.
- 최종 반영 결과는 `4173 preview` 기준 모바일/데스크톱 캡처로 보관했다.
- `2026-05-03` 로컬 preview 컨테이너(`http://127.0.0.1:4173`) 모바일 재검증에서는 `tabs.bottom = 651`, `textarea.bottom = 651`로 맞춰졌고, 마지막 줄까지 내부 스크롤로 확인됐다.
- 최종 반영 결과는 `preview.sm-home.cloud` 기준 모바일/데스크톱 캡처로 보관했다.
- 같은 날짜 후속 개선으로 루트/편집 셸/탭 본문을 다시 `auto + minmax(0, 1fr)` 구조로 복구하고, textarea를 `autoSize` 대신 탭 본문 남는 높이 전체를 채우는 방식으로 조정했다.
- `2026-05-03` `4173 preview` 최종 재검증에서는 모바일 기준 `bodyScrollHeight = 844`, `root.bottom = 844`, `textarea.bottom = 831`, `textarea.height = 487.17`로 남는 공간을 채우면서도 페이지 바깥 overflow는 발생하지 않았다.
- 같은 날짜 추가 미세조정 뒤 `4173 preview` 모바일 재검증에서는 `bodyScrollHeight = 664`, `root.bottom = 664`, `textarea.bottom = 599`, `textarea.height = 255.17`로 textarea 하단 여유를 더 확보했고, 페이지 바깥 overflow는 없었다.
- 같은 날짜 최신 재검증에서는 `test.sm-home.cloud`가 여전히 기존 번들로 `textarea.bottom = 747.25`인 반면, `4173 preview` 수정본은 `shell.bottom = 660`, `tabs.bottom = 655`, `textarea.bottom = 595`, `textarea.height = 272.17`로 wrapper 외곽과 하단 입력 영역이 더 안쪽에 들어오도록 정리됐다.
- `2026-05-03` 로컬 preview 컨테이너(`http://127.0.0.1:4173`) 최종 재검증에서는 모바일 기준 `bodyScrollHeight = 844`, `root.bottom = 844`, `textarea.bottom = 831`, `textarea.height = 487.17`로 남는 공간을 채우면서도 페이지 바깥 overflow는 발생하지 않았다.
- 같은 날짜 추가 미세조정 뒤 로컬 preview 컨테이너(`http://127.0.0.1:4173`) 모바일 재검증에서는 `bodyScrollHeight = 664`, `root.bottom = 664`, `textarea.bottom = 599`, `textarea.height = 255.17`로 textarea 하단 여유를 더 확보했고, 페이지 바깥 overflow는 없었다.
- 같은 날짜 최신 재검증에서는 `test.sm-home.cloud`가 여전히 기존 번들로 `textarea.bottom = 747.25`인 반면, `preview.sm-home.cloud` 수정본은 `shell.bottom = 660`, `tabs.bottom = 655`, `textarea.bottom = 595`, `textarea.height = 272.17`로 wrapper 외곽과 하단 입력 영역이 더 안쪽에 들어오도록 정리됐다.
- 이번 후속 수정 검증은 동일한 모바일 문제 이미지 기준으로 부모 wrapper 하단의 과한 빈 영역이 사라졌는지 확인하는 것이 목적이다.
- `2026-05-03` `4173 preview` 모바일 재검증에서는 `shell.bottom = 547.83`, `tabs.bottom = 542.83`, `textarea.bottom = 542.83`, `textarea.height = 220`으로 wrapper 자체가 이전보다 약 `292px` 짧아졌다.
- 같은 날짜 최신 재검증에서는 `4173 preview` 모바일 기준 `shell.bottom = 840`, `tabs.bottom = 836`, `textarea.bottom = 836`, `textarea.height = 521.17`로 다시 하단까지 거의 채우면서도 카드 외곽 하단 선이 캡처 안에서 유지됐다.
- 같은 날짜 최신 재검증에서는 `4173 preview` 모바일 기준 `shell.bottom = 806`, `tabs.bottom = 801`, `textarea.bottom = 771`, `textarea.height = 455.17`로 부모 카드 하단이 다시 화면 안에 들어오면서 textarea 높이도 이전 `v30`보다 `187px` 커졌다.
- 같은 날짜 최신 재검증에서는 `4173 preview` 모바일 기준 `shell.bottom = 794`, `tabs.bottom = 789`, `textarea.bottom = 765`, `textarea.height = 449.17`로 부모 카드 하단선을 `v31`보다 `12px` 더 위로 올리면서도 textarea 높이는 `6px`만 줄였다.
- 같은 날짜 최신 재검증에서는 `4173 preview` 모바일 기준 `shell.bottom = 778`, `tabs.bottom = 774`, `textarea.bottom = 754`, `textarea.height = 447.17`로 wrapper 하단선이 캡처 안에서 분명히 보이면서도 textarea 높이는 직전 대비 `2px`만 줄었다.
- 같은 날짜 원복 기준은 아이폰 12 Pro viewport에서 `test.sm-home.cloud``shell.bottom = 598`, `notes.bottom = 574`인 반면, `4173 preview` 수정본은 `shell.bottom = 616`, `notes.bottom = 600`으로 두번째 카드가 실제로 더 아래까지 내려오던 시점의 값으로 맞춘다.
- 이번 최신 조정 검증은 모바일 wrapper와 textarea를 동시에 더 늘리는 것이 목적이며, `4173 preview` 기준으로 다시 확인한다.
- 같은 날짜 최신 `v36` 검증에서는 아이폰 12 Pro viewport 기준 `4173 preview``shell.bottom = 586`, `tabs.bottom = 582`, `textarea.bottom = 578`, `textarea.height = 323.17`로 확인됐고, 같은 조건 `test.sm-home.cloud``shell.bottom = 564`, `tabs.bottom = 560`, `textarea.bottom = 548`, `textarea.height = 293.17`이었다.
- 같은 날짜 데스크톱 `4173 preview` 재검증에서는 `shell.bottom = 1088`, `tabs.bottom = 1077`, `textarea.bottom = 1077`로 기존 데스크톱 채움 구조는 유지됐다.
- 같은 날짜 데스크톱 `4173 preview` 재검증에서는 `shell.bottom = 1188`, `tabs.bottom = 1177`, `textarea.bottom = 1177`로 데스크톱 채움 구조가 그대로 유지됐다.
- 같은 날짜 후속 수정으로 `기능설명 입력` 탭은 `title input` 없이 textarea 하나만 남았고, `4173 preview` 기준 `titleInputCount = 0`, `textareaCount = 1`로 확인했다.
- `2026-05-03` 로컬 preview 컨테이너(`http://127.0.0.1:4173`) 모바일 재검증에서는 `shell.bottom = 547.83`, `tabs.bottom = 542.83`, `textarea.bottom = 542.83`, `textarea.height = 220`으로 wrapper 자체가 이전보다 약 `292px` 짧아졌다.
- 같은 날짜 최신 재검증에서는 `preview.sm-home.cloud` 모바일 기준 `shell.bottom = 840`, `tabs.bottom = 836`, `textarea.bottom = 836`, `textarea.height = 521.17`로 다시 하단까지 거의 채우면서도 카드 외곽 하단 선이 캡처 안에서 유지됐다.
- 같은 날짜 최신 재검증에서는 `preview.sm-home.cloud` 모바일 기준 `shell.bottom = 806`, `tabs.bottom = 801`, `textarea.bottom = 771`, `textarea.height = 455.17`로 부모 카드 하단이 다시 화면 안에 들어오면서 textarea 높이도 이전 `v30`보다 `187px` 커졌다.
- 같은 날짜 최신 재검증에서는 `preview.sm-home.cloud` 모바일 기준 `shell.bottom = 794`, `tabs.bottom = 789`, `textarea.bottom = 765`, `textarea.height = 449.17`로 부모 카드 하단선을 `v31`보다 `12px` 더 위로 올리면서도 textarea 높이는 `6px`만 줄였다.
- 같은 날짜 최신 재검증에서는 `preview.sm-home.cloud` 모바일 기준 `shell.bottom = 778`, `tabs.bottom = 774`, `textarea.bottom = 754`, `textarea.height = 447.17`로 wrapper 하단선이 캡처 안에서 분명히 보이면서도 textarea 높이는 직전 대비 `2px`만 줄었다.
- 같은 날짜 원복 기준은 아이폰 12 Pro viewport에서 `test.sm-home.cloud``shell.bottom = 598`, `notes.bottom = 574`인 반면, `preview.sm-home.cloud` 수정본은 `shell.bottom = 616`, `notes.bottom = 600`으로 두번째 카드가 실제로 더 아래까지 내려오던 시점의 값으로 맞춘다.
- 이번 최신 조정 검증은 모바일 wrapper와 textarea를 동시에 더 늘리는 것이 목적이며, `preview.sm-home.cloud` 기준으로 다시 확인한다.
- 같은 날짜 최신 `v36` 검증에서는 아이폰 12 Pro viewport 기준 `preview.sm-home.cloud``shell.bottom = 586`, `tabs.bottom = 582`, `textarea.bottom = 578`, `textarea.height = 323.17`로 확인됐고, 같은 조건 `test.sm-home.cloud``shell.bottom = 564`, `tabs.bottom = 560`, `textarea.bottom = 548`, `textarea.height = 293.17`이었다.
- 같은 날짜 데스크톱 `preview.sm-home.cloud` 재검증에서는 `shell.bottom = 1088`, `tabs.bottom = 1077`, `textarea.bottom = 1077`로 기존 데스크톱 채움 구조는 유지됐다.
- 같은 날짜 데스크톱 `preview.sm-home.cloud` 재검증에서는 `shell.bottom = 1188`, `tabs.bottom = 1177`, `textarea.bottom = 1177`로 데스크톱 채움 구조가 그대로 유지됐다.
- 같은 날짜 후속 수정으로 `기능설명 입력` 탭은 `title input` 없이 textarea 하나만 남았고, `preview.sm-home.cloud` 기준 `titleInputCount = 0`, `textareaCount = 1`로 확인했다.
- 이번 이관 작업은 패키지 내부 문서/리소스 구조 정리이며 동작 로직 추가 변경은 포함하지 않는다.

View File

@@ -0,0 +1,46 @@
import { FeatureMenuLayoutPage } from './feature-menu';
import { MemoLayoutPage } from './memo';
import { LayoutPlaygroundView } from '../../views/play/LayoutPlaygroundView';
import type { SavedLayoutRecord } from '../../views/play/layoutStorage';
type RenderSavedLayoutContentParams = {
layoutId: string;
layout: SavedLayoutRecord;
savedLayouts: SavedLayoutRecord[];
onSavedLayoutsChange?: (layouts: SavedLayoutRecord[]) => void;
};
function normalizeLayoutName(name: string) {
return name.replace(/\s+/g, '').trim();
}
export function isMemoSavedLayout(layout: Pick<SavedLayoutRecord, 'name'>) {
return normalizeLayoutName(layout.name).startsWith('메모');
}
export function isFeatureMenuSavedLayout(layout: Pick<SavedLayoutRecord, 'name'>) {
return normalizeLayoutName(layout.name).startsWith('기능설명관리');
}
export function renderSavedLayoutContent({
layoutId,
layout,
savedLayouts,
onSavedLayoutsChange,
}: RenderSavedLayoutContentParams) {
if (isMemoSavedLayout(layout)) {
return <MemoLayoutPage layoutId={layoutId} />;
}
if (isFeatureMenuSavedLayout(layout)) {
return (
<FeatureMenuLayoutPage
layoutId={layoutId}
savedLayouts={savedLayouts}
onSavedLayoutsChange={onSavedLayoutsChange ?? (() => undefined)}
/>
);
}
return <LayoutPlaygroundView savedLayoutViewId={layoutId} onSavedLayoutsChange={onSavedLayoutsChange} />;
}

View File

0
src/features/layout/widget-registry-gallery/index.ts Executable file → Normal file
View File

View File

@@ -7,12 +7,18 @@ export type SampleWidgetsLayoutProps = {
entries: SampleEntry[];
pathFilter?: string;
includeComponentIds?: string[];
includeSampleIds?: string[];
disableWidgetCardWrapper?: boolean;
singlePreviewMode?: boolean;
};
export function SampleWidgetsLayout({
entries,
pathFilter = '/widgets/',
includeComponentIds = [],
includeSampleIds = [],
disableWidgetCardWrapper = false,
singlePreviewMode = false,
}: SampleWidgetsLayoutProps) {
const [sampleEntries, setSampleEntries] = useState<LoadedSampleEntry[]>([]);
@@ -30,25 +36,38 @@ export function SampleWidgetsLayout({
};
}, [entries, pathFilter]);
const visibleEntries =
includeComponentIds.length > 0
? sampleEntries.filter((entry) => includeComponentIds.includes(entry.sampleMeta.componentId))
: sampleEntries;
const visibleEntries = sampleEntries.filter((entry) => {
if (includeComponentIds.length > 0 && !includeComponentIds.includes(entry.sampleMeta.componentId)) {
return false;
}
if (includeSampleIds.length > 0 && !includeSampleIds.includes(entry.sampleMeta.id)) {
return false;
}
return true;
});
if (visibleEntries.length === 0) {
return <Empty description="표시할 위젯 샘플이 없습니다." />;
}
return (
<Flex gap={20} wrap className="sample-widgets-layout">
<Flex
gap={singlePreviewMode ? 0 : 20}
wrap={!singlePreviewMode}
className={`sample-widgets-layout${singlePreviewMode ? ' sample-widgets-layout--single-preview' : ''}`}
>
{visibleEntries.map(({ modulePath, Sample, sampleMeta }) => (
<div
key={modulePath}
id={`widget-sample-${sampleMeta.componentId}`}
className="sample-widgets-layout__item"
className={`sample-widgets-layout__item${singlePreviewMode ? ' sample-widgets-layout__item--single-preview' : ''}`}
data-focus-id={`widget:${sampleMeta.componentId}`}
>
<div className="sample-widgets-layout__item">{createElement(Sample)}</div>
<div className={`sample-widgets-layout__item${singlePreviewMode ? ' sample-widgets-layout__item--single-preview' : ''}`}>
{createElement(Sample, { disableWidgetCardWrapper })}
</div>
</div>
))}
</Flex>

0
src/features/layout/widget-sample-gallery/index.ts Executable file → Normal file
View File