Files
HMDesign/packages/sw/ai/COMPOSITION_PATTERNS.md
T
shkim4 caf4c32592 feat(sw): viewer layer eye-icon rule, header hover, trailing strip and file name width
User-requested SW Viewer changes (2026-10-21); no new Props, public .d.ts unchanged.
- Layer popup visibility: AI docs (VIEWER §4.1, catalog, design rules, patterns,
  AI_GUIDE) require the Eye / EyeOff icon button, never a switch; example H now
  renders a real layer list with eye buttons; Storybook eye button gets the pointer.
- Viewer header hover: text and icon #FFC600, the #266651 hover face removed; size and
  position unchanged. Status stays display only.
- Trailing strip: status, the trailing icon button (info / license) and SWHeader
  minimize / close are full-height (48px) segments on the window-area face, no gap, top
  and bottom on the object surface border; no line around the license button.
- File name: the measured steps use the file-name basis (header width), not the text
  width, so a short and a long name give the same Open / file-name widths.
- File open icon: the black icon outline of every viewer icon (SWViewerIcon), no frame.
2026-10-21 18:52:38 +09:00

21 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·10은 SW 일반 모드, 7은 SW 뷰어 모드이며 한 화면에서 섞지 않습니다. 11(테마 설정)은 두 모드 공통입니다.

요청 모드 패턴
기존 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. 로고 클릭 메뉴
화면 하단 명령 막대(Line·Arc 등 명령, 분류 탭, 보조기능 토글, 명령창), Model/Layout 탭 일반 10. Footer
글꼴 등 SW 토큰을 화면 전체(Header·Dropdown·More·Panel·명령창)에 같은 값으로 바꾸기, Portal 요소 포함 일반·뷰어 공통 11. 테마(토큰) 설정

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. 레이어·색상·선스타일은 SWPopupPropertyRow(속성창 picker)로 둔다. SWHeader 필드 Dropdown(SWHeaderLayerMenu·SWHeaderColorMenu)은 Header 전용이라 Popup에 넣지 않는다. 게이지바는 SWSlider(appearance "panel" 기본 / "property" 속성창 모양), 선택 목록은 SWSelect를 SWPopupRow field="none"에 둔다. SW에 없는 컨트롤(Switch, 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>
    <SWPopupSection title="표시">
      <SWPopupRow label="단위" htmlFor={unitId} field="none"><SWSelect id={unitId} value={unit} options={[{ value: "mm" }, { value: "m" }]} onValueChange={setUnit} /></SWPopupRow>
      <SWPopupPropertyRow property={{ id: "layer", name: "레이어", value: layerId, type: "select", groupId: "g", picker: "layer", options }} menus={{ layers }} onPropertyChange={(_, id) => setLayerId(id)} />
      <SWPopupRow label="투명도" field="none" unit="%"><SWSlider aria-label="투명도" value={opacity} onValueChange={setOpacity} onValueCommit={applyOpacity} /></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 …)를 섞지 않습니다(예외: 창 최소화·닫기 버전의 trailing 끝 SWHeaderWindowControls, VIEWER.md §2.1).

const groups: SWViewerHeaderGroup[] = [
  { 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: [
    // 실제 가용 폭: 공간이 있으면 표시 → 아이콘만 → More (숫자는 순서만 정함).
    { type: "button", id: "fit", label: "전체 보기", icon: icons.fit, iconOnlyBelow: 560, collapseBelow: 1180, 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, iconOnlyBelow: 900, onPressedChange: setOptionsOpen },
  ] },
]

<SWViewerHeader documentTitle={fileName} groups={groups} onCommand={runCommand}
  onDocumentOpen={openFile} documentOpenLabel="파일 열기"
  documents={samples} activeDocumentId={sampleId} onDocumentSelect={loadSample}
  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>
구성 사용
파일 열기·샘플 로고 오른쪽 파일 열기 버튼 onDocumentOpen·documentOpenLabel(SWHeader documentOpenPlacement="header" 면 + 테두리 아이콘, 공간이 부족하면 아이콘만, More로 가지 않음), 파일명 클릭 documents·activeDocumentId·onDocumentSelect(SWHeader 파일명 목록, Open·+ New 없음)
Header 메뉴 groups 데이터: 도면 공간(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. 레이어 표시·숨김은 눈(Eye/EyeOff) 아이콘 버튼, Switch 금지(VIEWER.md §4.1, 예제 H)
반응형 고정 너비가 아니라 Header의 실제 가용 폭으로 3단계: 기본(아이콘+텍스트) → iconOnlyBelow 항목 모두 아이콘만(툴팁 유지) → collapseBelow 항목을 하나씩 More로(값이 큰 것, 같으면 오른쪽 먼저). 넓어지면 역순 복귀. 패널 토글은 collapseBelow 없이 아이콘까지만
우클릭 메뉴 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를 만들지 않는다.

상세는 FOOTER.md. 화면 = SWHeader → 도면 영역 → SWFooter(바 안 명령창 command, 위 왼쪽 좌표 status, 위 오른쪽 SWFooterLayoutTabs). 실제 SW 화면(2026-10-20) 구성입니다.

<div className="app-screen">{/* 소비 레이아웃: flex column, 높이 100% */}
  <SWHeader documentTitle={title} />
  <main className="app-canvas">{/* flex: 1 */}</main>
  <SWFooter status={coordinates}
    command={<SWFooterCommandLine history={history} onSubmit={run} expanded={listOpen} onExpandedChange={setListOpen} />}
    categories={categories} categoryId={categoryId} onCategoryChange={setCategoryId}
    activeToolId={running} onToolSelect={toolId => run(toolId)} onSlotAdd={openRegister}
    auxItems={aux} onAuxPressedChange={toggleAux} commandAll={{ onClick: () => setListOpen(open => !open), pressed: listOpen }}
    layoutTabs={<SWFooterLayoutTabs layouts={layouts} value={space} onValueChange={setSpace} onAdd={addLayout} onRemove={removeLayout} onRename={renameLayout} />} />
</div>
  1. 명령·분류·보조기능은 데이터입니다. 프로젝트마다 바꾸고 Footer 외형은 그대로 둡니다.
  2. 명령 실행·등록·Layout 생성은 Callback에서 호스트가 처리하고 Props를 갱신합니다(activeToolId, categories, layouts).
  3. Figma 아이콘은 swFooterToolIcons·swFooterAuxIcons, 새 명령 아이콘은 프로그램 제공 18 × 18 요소.
  4. 좁은 화면 처리(빈 칸 축소, ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만)는 SWFooter가 합니다. 소비 코드에서 명령 수를 줄이거나 media query로 다시 배치하지 않습니다.
  5. 일부만 필요하면(보조기능만, 분류 탭만) <SWTheme> 안에서 SWFooterAuxGroup·SWFooterMenu·SWFooterToolButton을 씁니다. SWFooterMenu 단독은 부모 면을 var(--hm-sw-footer-group-background)로 둡니다.

11. 테마(토큰) 설정 — ExampleJ-Theme.tsx

일반·뷰어 모드 공통입니다. 토큰 기본값은 가장 바깥 SW 범위에서만 선언되고 안쪽 SW 요소는 상속합니다(AI_USAGE.md §3.1).

const appSWTheme = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' } as CSSProperties // 프로그램이 정한 값만

<SWTheme style={appSWTheme}>              {/* 가장 바깥 범위: 여기서만 지정 */}
  <SWViewerHeader … />                    {/* Dropdown·More까지 상속 */}
  <SWViewerPanel …>…</SWViewerPanel>      {/* 패널·안쪽 컨트롤 상속 */}
</SWTheme>

{createPortal(<SWTheme style={appSWTheme}>…</SWTheme>, document.body)} {/* Portal은 같은 객체로 다시 감쌈 */}
  • 일반 모드도 같습니다: <SWTheme style={appSWTheme}> 안의 SWHeader(Dropdown·창 이동 Tooltip)·SWFooter(SWFooterCommandLine·명령 등록 안내)가 같은 값을 씁니다.
  • 기본 글꼴은 Noto Sans KR입니다(2026-10-20 확정). 기본값을 쓸 때는 글꼴 토큰을 지정하지 않습니다. Web UI 글꼴 토큰(--hm-font-family-base)을 SW 화면에 넣지 않습니다.
  • hm-sw-* class 선택자로 토큰을 덮거나 !important, 컴포넌트마다 같은 값 반복 지정을 하지 않습니다.

9. 하지 말 것

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