Files
HMDesign/packages/sw/ai/COMPOSITION_PATTERNS.md
T
shkim4 3a5e29f2fa Merge branch 'integration/sw-home-menu' into feature/sw-home-menu
Integrate the SW home (logo) menu into SWHeader for SW general mode only.
SW viewer mode code and stories are unchanged.

- Resolve conflicts: keep both sw-home-menu and sw-viewer exports;
  renumber AI docs (catalog viewer 8->9, patterns home menu 7->8,
  design rules home menu 11->12)
- SWHeader: start the home menu after the file name when it sits
  inside controls, so the file name stays visible
- AI guides: mark home menu as general-mode only; forbid it in viewer
  mode (AI_USAGE, catalog, design rules, patterns, VIEWER.md, AI_GUIDE)
- Tests: ignore wildcard identifiers in ai-docs check; update expected
  Storybook SW titles (SW/Tool removed, SW/Viewer added)
- Add merge QA report docs/sw/sw-home-menu-merge-2026-10-19.md
2026-10-19 18:21:19 +09:00

16 KiB

SW 조합 패턴

모든 패턴은 공개 Export만 사용합니다. 전체 코드는 examples/에 있으며, 설치된 패키지에서 꺼낸 그대로 독립 React 19 + TypeScript + Vite 프로젝트의 tsc·Production Build·브라우저 검증을 통과했습니다(2026-10-19, 예제 A~F. 뷰어 모드 예제 H는 공개 Export만 쓰지만 같은 독립 소비 검증 기록은 이 문서에 없습니다).

0. 어떤 패턴을 고를까

먼저 SW 모드를 정합니다(AI_USAGE.md §0). 1~6과 8은 SW 일반 모드, 7은 SW 뷰어 모드이며 한 화면에서 섞지 않습니다.

요청 모드 패턴
기존 SW Header 전체(일부 부품 제거·메뉴 순서 변경 포함) 일반 1. 완성형
기존 컨트롤 일부만, 다른 배치·순서 일반 2. 조합 Header
기존 Dropdown을 툴바·패널 등 다른 곳에 일반 3. 개별 컨트롤
패키지에 없는 기능 일반(뷰어는 7 + AI_USAGE §0.4) 4. 확장(소비 프로젝트 신규 컴포넌트)
SW Popup 디자인의 설정창·정보창·사용자 정의 폼 일반 5. Popup(외형 고정, 콘텐츠 자유)
일반 모드 SWHeader에 로고 옆 파일 열기·도구 막대(Model/Layout, Fit, 2.5D) 추가 일반 6. 파일 열기 + 도구 막대
뷰어·DWG/DXF 보기·도면 뷰어·읽기 전용 화면(Header 메뉴, 패널, 모달, 불러오기 상태) 뷰어 7. 뷰어 모드 기본 조합
일반 모드 SWHeader에서 로고를 누르면 열리는 2단 메뉴(저장·내보내기·찾아열기·최근파일 등) 일반 8. 로고 클릭 메뉴

1. 완성형 SW Header — ExampleA-DefaultHeader.tsx

<SWHeader documentTitle={name} documents={documents} layers={layers} colors={colors} onClose={close} />
  • 부품 제거: hiddenControls={["print", "popupGuide"]}(로고는 항상 표시, "home"은 정적 로고).
  • 메뉴 순서: menuOrder={["color", "layer"]}(5개 메뉴끼리, 누락분은 기본 순서로 뒤에).
  • 객체 영역 전체를 직접 나열: controls={[{ id: "document" }, { id: "layer" }, { id: "print" }]}.
  • 추가 도구: tools={<MyTool />}(객체 영역 메뉴 뒤). 새 도구는 아래 4의 규칙을 따른다.
  • 이 경우에만 반응형 More가 동작합니다.

2. 조합 Header (간소화·기능 중심)

<SWTheme>을 Header 막대로 쓰고, 막대·면 배치 CSS는 소비 프로젝트가 작성합니다(example-layout.css). 이 CSS는 --hm-sw-header-* 토큰만 읽고 hm-sw-* class를 건드리지 않습니다.

<SWTheme className="app-sw-bar">                 {/* 49px, #060606, flex, gap 2px */}
  <SWHeaderHome logoAlt="뷰어" onClick={goHome} />  {/* 80px, 다음 파일 면과 겹침 */}
  <SWHeaderDocumentTitle title={name} documents={documents} onDocumentSelect={open} />
  <div className="app-sw-surface">                 {/* 객체 수정 면 토큰 */}
    <SWHeaderLayerMenu value={layer} layers={layers} onLayerSelect={selectLayer} />
    <SWHeaderColorMenu value={color} colors={colors} onColorSelect={selectColor} />
  </div>
  <div className="app-sw-window">                  {/* 창 영역 면 토큰 */}
    <SWHeaderActions showPrint={false} onPopupGuide={showGuide} />
  </div>
</SWTheme>
  • 간소화 Header(일반 모드): ExampleB-ViewerHeader.tsx — 로고 → 파일 → 레이어 → 색상 → K. 파일명·주석의 "Viewer"는 과거 이름이며 SW 뷰어 모드가 아닙니다(뷰어 모드는 7).
  • 기능 중심: ExampleC-FeatureHeader.tsx — 파일 → 선 → 문자 → 치수스타일 → 인쇄.
  • 순서는 JSX 순서입니다(DOM·Tab 순서 동일). CSS order로 바꾸지 않습니다.
  • 로고 없이 파일 면이 처음이면 왼쪽 테두리가 없으므로 wrapper에 1px 테두리를 둡니다(예제 C app-sw-file-edge).
  • K만/인쇄만: SWHeaderActions의 showPrint={false} / showPopupGuide={false}.
  • 창 제어: <SWHeaderWindowControls onWindowMoveStart={startDrag} onMinimize={min} onClose={close} />(창 영역 면 안).
  • 조합 Header에는 반응형 More가 없습니다. 좁은 화면이 필요하면 완성형을 쓰거나 표시할 컨트롤을 줄입니다.

3. 기존 Dropdown을 다른 레이아웃에서

<SWTheme className="app-toolbar">
  <SWHeaderLineStyleMenu value={lineStyle} lineTypes={lineTypes} lineWeights={lineWeights} onLineTypeSelect={setType} />
</SWTheme>
  • Dropdown은 필드 바로 아래에 열리고 뷰포트 안으로 좌우 보정됩니다. 부모에 overflow: hidden을 두지 않습니다.
  • Controlled가 필요하면 open / onOpenChange를 함께 줍니다.
  • Dropdown이 필요 없고 호스트가 자체 팝업을 열면 SWHeaderLayerControl 등 필드 버튼만 씁니다.

4. 확장 — 없는 컨트롤 새로 만들기 — SWSearchControl.tsx, ExampleD-SearchExtension.tsx

요청 예: "레이어와 색상은 기존 SW 컴포넌트, 검색 버튼은 SW 디자인으로 새로."

  1. 카탈로그 검색: 검색 컴포넌트 없음(MISSING).
  2. 조합 검토: 텍스트 입력·확정·취소·IME는 SWHeaderInput으로 충족 → 재사용. 필드 면과 검색 아이콘 버튼은 공개 컴포넌트가 없음 → 신규.
  3. 신규 부분은 소비 프로젝트에 작성: 38px 속성 필드 면, 12px 라벨, gradient Hover 링, 아이콘 버튼은 글자색 Hover #FFC600. 모든 값은 토큰(sw-search-control.css).
  4. 기존 컨트롤과 같은 면(app-sw-surface)에 나란히 배치.
  5. 검증: 레이어 필드와 높이·위치·테두리·radius·배경·그림자·라벨 크기 일치, Hover 링, Enter 확정 → onSearch, Escape 복원.

새 컨트롤은 패키지 공식 컴포넌트로 자동 등록하지 않습니다. 여러 프로젝트에서 반복되면 SW 패키지 승격을 별도로 검토합니다.

5. Popup — 외형 고정, 콘텐츠 자유 — ExampleE-CustomPopup.tsx

요청 예: "HMDesign SW Popup 디자인을 사용해서 내부에 새로운 설정 폼을 만들어 줘."

  1. 카탈로그 3절에서 SWPopup을 찾는다(프레임 = 속성창 외형·닫기·Escape·스크롤).
  2. 내용은 children으로 구성한다. 행 모양은 SWPopupSection + SWPopupRow, 값 입력은 SWHeaderInput.
  3. SW에 없는 컨트롤(Switch, Slider, Select, Button 등)은 @hm-design/ui를 children 안에 그대로 둔다. 구조 제한은 없다.
  4. 프레임(폭, 제목줄, 닫기, 표면색)을 다시 만들지 않고, hm-sw-property* class를 쓰지 않는다.
<SWTheme>
  <SWPopup title="도면 설정" onClose={close}>
    <SWPopupSection title="도면">
      <SWPopupRow label="이름" htmlFor={id}><SWHeaderInput id={id} value={name} onCommit={setName} /></SWPopupRow>
      <SWPopupRow label="축척" unit="%" numeric><SWHeaderInput aria-label="축척" valueType="number" value={scale} onCommit={setScale} /></SWPopupRow>
    </SWPopupSection>
    <MyProjectFields />
  </SWPopup>
</SWTheme>
  • 데이터 기반 속성창이 필요하면 SWHeaderPropertyPanel(properties / groups)을 쓰고, 추가 내용은 같은 컴포넌트의 children으로 붙인다.
  • 위치는 호스트가 정한다: 문서 흐름(style) 또는 anchorRef(기준 요소 아래 고정 Overlay). 열고 닫는 상태도 호스트가 가진다(조건부 렌더링).
  • 여러 Popup을 동시에 띄워도 Escape는 하나씩 닫힌다(카탈로그 3절).

6. 파일 열기 + 도구 막대 — ExampleF-ViewerToolbar.tsx

일반 모드 Header에 도구 막대를 더한 구성입니다. 예제 파일명·주석의 "viewer"는 과거 이름이며 SW 뷰어 모드가 아닙니다. "뷰어"·"도면 보기" 화면은 7(SWViewer*)을 씁니다.

<SWHeader documentTitle={name} documents={documents} documentOpenPlacement="header" documentOpenLabel="파일 열기" onDocumentOpen={open}
  controls={[{ id: "document" }, { id: "layer" }, { id: "color" }, { id: "popupGuide" }]}
  hiddenControls={["documentNew", "layerFreeze", "layerLock"]}
  tools={<SWToolGroup label="보기" selection="single">
    <SWToolButton pressed={view === "model"} onClick={showModel}>Model</SWToolButton>
    <SWToolButton pressed={view === "layout"} onClick={showLayout}>Layout</SWToolButton>
  </SWToolGroup>} />
  • 파일 열기는 tools에 직접 만들지 않는다. documentOpenPlacement="header"가 로고 바로 오른쪽에 파일명과 같은 면으로 그리고 Dropdown의 Open은 숨긴다(중복 없음). 공간이 부족하면 More로 이동한다.
  • 지원하지 않는 기능은 hiddenControls로 뺀다(documentNew, documentOpen, layerFreeze, layerLock). 가짜 버튼을 두지 않는다.
  • SWHeader 없이 같은 순서가 필요하면: SWHeaderHome → SWToolButton appearance="document" → SWHeaderDocumentTitle showOpen={false}(사선 접점·12px 여백은 패키지가 처리). 이 경우 More는 없다.

7. 뷰어 모드 기본 조합 — ExampleH-ViewerMode.tsx

SW 뷰어 모드(도면 보기) 화면입니다. 모드 선택은 AI_USAGE.md §0, Props·상태는 VIEWER.md, 토큰은 DESIGN_RULES.md §11. 이 패턴에는 1~6의 일반 모드 컴포넌트(SWHeader, SWPopup, SWToolButton …)를 섞지 않습니다.

const groups: SWViewerHeaderGroup[] = [
  { id: "file", label: "파일", items: [
    { type: "button", id: "open", label: "파일 열기", icon: icons.open, iconOnlyBelow: 560 },
    { type: "menu", id: "samples", label: "샘플", moreLabel: "샘플 도면", collapseBelow: 1180, entries: sampleEntries },
  ] },
  { id: "space", label: "도면 공간", surface: "field", hidden: !fileName, items: [
    { type: "tabs", id: "space", label: "모델과 배치 선택", collapseBelow: 1180, value: space, onValueChange: setSpace, options },
  ] },
  { id: "view", label: "화면 보기", surface: "field", items: [
    { type: "button", id: "fit", label: "전체 보기", icon: icons.fit, iconOnlyBelow: 560, disabled: !fileName },
    { type: "switch", id: "depth", label: "2.5D", checked: depth, collapseBelow: 1180, onCheckedChange: setDepth },
  ] },
  { id: "panels", label: "패널", placement: "end", items: [
    { type: "toggle", id: "options", label: "설정", icon: icons.options, pressed: optionsOpen, collapseBelow: 900, onPressedChange: setOptionsOpen },
  ] },
]

<SWViewerHeader documentTitle={fileName} groups={groups} onCommand={runCommand}
  trailing={<>
    <SWViewerStatus state={fileName ? "ready" : "idle"} />
    <SWViewerHeaderButton iconOnly icon={icons.info} label="정보" onClick={() => setAbout(true)} />
  </>} />
<SWViewerPanel title="옵션" open={optionsOpen} onClose={() => setOptionsOpen(false)} width={340} height="auto"
  footer={<SWViewerButton variant="primary" onClick={apply}>적용</SWViewerButton>}>
  <SWViewerSection title="표시">
    <SWViewerField label="그리드">{id => <SWViewerSwitch id={id} checked={grid} onCheckedChange={setGrid} />}</SWViewerField>
    <SWViewerField label="줌 속도" layout="stacked" below={<SWViewerSlider aria-label="줌 속도" value={zoom} min={0.2} max={3} step={0.1} onValueChange={setZoom} />}>
      {id => <SWViewerInput id={id} type="number" value={zoom} onChange={e => setZoom(Number(e.currentTarget.value))} />}
    </SWViewerField>
  </SWViewerSection>
</SWViewerPanel>
<SWViewerPanel modal title="정보" open={about} onClose={() => setAbout(false)} width={360} height="auto" resizable={false} collapsible={false}
  footer={<SWViewerButton variant="primary" onClick={() => setAbout(false)}>확인</SWViewerButton>}>…</SWViewerPanel>
구성 사용
Header 메뉴 groups 데이터: 파일(button·menu), 도면 공간(tabs, surface: "field"), 화면 보기(button·switch, field), 패널 토글(toggle, placement: "end")
오른쪽 끝 trailing: SWViewerStatus + SWViewerHeaderButton iconOnly
도구 창 비모달 SWViewerPanel(드래그·크기 조절·접기 기본). 여러 개면 inset·defaultPosition으로 겹치지 않게
확인·정보 SWViewerPanel modal + footer의 SWViewerButton variant="primary"
패널 내용 SWViewerSection > SWViewerField > SWViewerSwitch / SWViewerSlider / SWViewerInput, 목록은 surface="plain" + 프로그램 목록, 설명 SWViewerHint
반응형 참고값 collapseBelow 1180(샘플·도면 공간·2.5D) / 900(패널 토글), iconOnlyBelow 560(파일 열기·전체 보기)
우클릭 메뉴 SWViewerMenu + anchorPoint
  • 상태(파일명·패널 열림·탭 값)는 프로그램이 가지고, 명령은 각 Callback 또는 onCommand 한 곳에서 받는다. 도면 영역 높이·렌더러 크기는 앱 레이아웃 담당(VIEWER.md §7).
  • 없는 기능은 메뉴를 만들지 않거나 disabled + 미연결 보고. 패키지에 없는 UI는 AI_USAGE.md §0.4 뷰어 열을 따른다.

8. 로고 클릭 메뉴 (일반 모드) — ExampleG-HomeMenu.tsx

SW 일반 모드 SWHeader 전용입니다. 패턴 7(뷰어 모드)에는 홈 메뉴를 넣지 않습니다.

const sections: SWHomeMenuSectionData[] = [
  { id: "working", title: "작업중인 파일", items: [
    { id: "save", label: "저장", icon: <SaveIcon />, toggle: false, onSelect: save },
  ] },
  { id: "open", title: "다른 파일 열기", items: [
    { id: "find-open", label: "찾아열기", icon: <OpenIcon />, toggle: false, onSelect: openFile },
    { id: "recent", label: "최근파일", icon: <RecentIcon />, panel: <SWRecentFiles files={recent} onFileOpen={openRecent} /> },
  ] },
]
<SWHeader documentTitle={name} documents={documents} onDocumentOpen={openFile} onDocumentNew={newFile}
  homeMenu={{ sections, onItemSelect }} onHomeMenuOpenChange={(open, reason) => log(open, reason)} />
  1. 카탈로그 §5의 데이터로 메뉴를 쓴다. 화면마다 타일·섹션·제목을 따로 만들지 않는다.
  2. 메뉴 데이터 변경: 항목 추가·삭제는 items 배열, 숨김은 hidden, 비활성은 disabled, 실행만 하는 버튼은 toggle: false, 켜고 끄는 버튼은 기본값. Active를 직접 제어하려면 homeMenu.activeIds + onActiveChange.
  3. 최근파일 패널은 선택 기능이다. 필요한 타일에만 panel: <SWRecentFiles … />를 준다. 파일을 열면 메뉴가 닫힌다(closeOnOpen).
  4. 로고 클릭은 메뉴만 연다. onHomeClick의 홈 이동은 homeMenu가 있으면 실행되지 않는다. 메뉴 항목에 홈 이동이 필요하면 항목 onSelect에서 한다.
  5. New / Open은 문서 Dropdown에 그대로 둔다(documentOpenPlacement 기본 "menu"). 로고 오른쪽에 별도 Open 버튼을 두지 않는다. 메뉴의 찾아열기·새파일은 같은 handler를 연결한다.
  6. 위치·폭은 SWHeader가 정한다. 소비 코드에서 메뉴 위치를 계산하거나 겹치는 wrapper를 만들지 않는다.

9. 하지 말 것

  • hm-sw-* class를 소비 코드에 쓰거나 CSS로 덮어쓰기
  • dist 구현·styles.css 블록 복사로 같은 컴포넌트 재작성
  • @hm-design/sw/dist/...·src/... 내부 경로 Import
  • 문서에 없는 Props 추측, 새 색·크기 값 도입
  • 같은 화면에 SWHeader와 조합 Header를 겹쳐 두 개의 Header 만들기
  • 같은 화면에 SWHeader와 SWViewerHeader를 함께 두기, 일반 모드 화면에 SWViewer* 사용, 뷰어 화면에서 SWHeader·SWPopup 재스타일(AI_USAGE §0.3)