v7.0.0 release
This commit is contained in:
1 parent
0e7fa04b03
commit
c4ea9a6cd1
3582 files changed
+626189
-160151
No files matched your search
@@ -1,3 +1,4 @@
|
||||
// e2e:allow편집기 passthrough 결함#2 수정(admin 동기, data-editor-*/id 를 시각적 루트 Div 로 라우팅). 단위 회귀는 FileInput.test.tsx passthrough describe.
|
||||
import React, { forwardRef, useRef, useCallback, useState } from 'react';
|
||||
import { Div } from './Div';
|
||||
import { Span } from './Span';
|
||||
@@ -40,6 +41,20 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(({
|
||||
const inputRef = (ref as React.RefObject<HTMLInputElement>) || internalRef;
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
|
||||
// 레이아웃 편집기 식별 표식(data-editor-*) 과 DOM id 는 시각적 루트(<Div>)에 부착해야
|
||||
// 캔버스에서 선택/편집이 닿는다. basic 컴포넌트라 DynamicRenderer 가 개별 data-editor-*
|
||||
// 키를 props 로 주입하는데, 숨겨진 <Input className="hidden"> 는 0×0 이라 식별 표식이 닿아도
|
||||
// 선택 불가. 식별/표식 키만 분리해 루트로 보낸다.
|
||||
const rootEditorProps: Record<string, unknown> = {};
|
||||
const inputProps: Record<string, unknown> = {};
|
||||
Object.entries(props as Record<string, unknown>).forEach(([key, val]) => {
|
||||
if (key.startsWith('data-editor-') || key === 'id' || key === 'onMouseMove' || key === 'onMouseLeave') {
|
||||
rootEditorProps[key] = val;
|
||||
} else {
|
||||
inputProps[key] = val;
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!disabled && inputRef.current) {
|
||||
inputRef.current.click();
|
||||
@@ -81,7 +96,7 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(({
|
||||
}, [onChange, inputRef]);
|
||||
|
||||
return (
|
||||
<Div className={`flex items-center gap-2 ${className}`}>
|
||||
<Div className={`flex items-center gap-2 ${className}`} {...rootEditorProps}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
@@ -89,7 +104,7 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(({
|
||||
onChange={handleChange as any}
|
||||
disabled={disabled}
|
||||
className="hidden"
|
||||
{...props}
|
||||
{...inputProps}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -320,8 +320,22 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
// HTML 표준 속성이 아닌 것들을 필터링 (타입 안전성 개선)
|
||||
const { loadingactions, formdata, ...validProps } = props as typeof props & NonStandardProps;
|
||||
|
||||
// 레이아웃 편집기 표식/핸들러(data-editor-*, onMouseMove/Leave 등)는 시각적
|
||||
// 루트(wrapper div)에 부착해야 캔버스 선택/드래그가 컴포넌트 전체를 잡는다.
|
||||
// 내부 input 에만 spread 하면 선택 박스가 input 으로 한정되고 error/검증/토글이
|
||||
// 선택 영역 밖이 된다.
|
||||
const editorRootProps: Record<string, unknown> = {};
|
||||
const inputRestProps: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(validProps as Record<string, unknown>)) {
|
||||
if (k.startsWith('data-editor-') || k === 'onMouseMove' || k === 'onMouseLeave') {
|
||||
editorRootProps[k] = v;
|
||||
} else {
|
||||
inputRestProps[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative ${wrapperClassName}`}>
|
||||
<div className={`relative ${wrapperClassName}`} {...editorRootProps}>
|
||||
{/* 입력 필드 wrapper */}
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -334,7 +348,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
onKeyDown={handleKeyEvent}
|
||||
onKeyUp={handleKeyEvent}
|
||||
disabled={disabled}
|
||||
{...validProps}
|
||||
{...inputRestProps}
|
||||
/>
|
||||
|
||||
{/* 비밀번호 보기/숨기기 버튼 */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// e2e:allow커스텀 드롭다운 루트 props spread 타입 안전 캐스팅(admin 동기). 단위 회귀는 Select.test.tsx.
|
||||
import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Div } from './Div';
|
||||
@@ -28,6 +29,12 @@ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectE
|
||||
searchable?: boolean;
|
||||
/** 검색 input placeholder */
|
||||
searchPlaceholder?: string;
|
||||
/**
|
||||
* 레이아웃 편집기 주입 속성 (편집 모드 전용). Select 는 커스텀 드롭다운 루트에
|
||||
* 개별 data-editor-* 키를 spread 해야 선택/편집이 닿는다(커스텀 루트
|
||||
* passthrough). basic 컴포넌트라 `{...props}` 로 DOM 표준 속성(id 등)도 함께 흐른다.
|
||||
*/
|
||||
editorAttrs?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +74,7 @@ export const Select: React.FC<SelectProps> = ({
|
||||
disabled,
|
||||
searchable = false,
|
||||
searchPlaceholder,
|
||||
editorAttrs,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -237,6 +245,7 @@ export const Select: React.FC<SelectProps> = ({
|
||||
value={value}
|
||||
onChange={onChange as React.ChangeEventHandler<HTMLSelectElement>}
|
||||
disabled={disabled}
|
||||
{...editorAttrs}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -252,8 +261,13 @@ export const Select: React.FC<SelectProps> = ({
|
||||
? `${className}${hasTextColor ? '' : ' text-gray-700 dark:text-gray-200'}`
|
||||
: `w-full px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl text-gray-700 dark:text-gray-200 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none ${className}`;
|
||||
|
||||
// props 는 SelectHTMLAttributes 기반이라 Div(HTMLDivElement) 에 직접 spread 시 타입 불일치.
|
||||
// 런타임 동작(편집기 data-editor-* / id 등 DOM 표준 속성 passthrough)은 동일하므로
|
||||
// DOM-safe 레코드로 캐스팅해 커스텀 드롭다운 루트에 전달한다.
|
||||
const rootDomProps = { ...editorAttrs, ...props } as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
<Div ref={containerRef} className="relative">
|
||||
<Div ref={containerRef} className="relative" {...rootDomProps}>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import { FileInput } from '../FileInput';
|
||||
|
||||
describe('FileInput 컴포넌트 (sirsoft-basic)', () => {
|
||||
// ========================================
|
||||
// 레이아웃 편집기 passthrough
|
||||
// ========================================
|
||||
// 편집 모드에서 basic 컴포넌트에 주입되는 data-editor-* / id 는 시각적 루트(<div>)에
|
||||
// 부착되어야 한다. 숨겨진 <input class="hidden"> (0×0) 에 부착되면 캔버스에서 선택 불가.
|
||||
describe('편집기 passthrough (data-editor-* / id → 시각적 루트 <div>)', () => {
|
||||
it('data-editor-* 표식이 숨겨진 input 이 아닌 루트 div 에 부착된다', () => {
|
||||
const { container } = render(
|
||||
<FileInput
|
||||
data-editor-name="FileInput"
|
||||
data-editor-path="1.children.0"
|
||||
id="fi-root-id"
|
||||
/>
|
||||
);
|
||||
|
||||
const root = container.querySelector('[data-editor-name="FileInput"]') as HTMLElement;
|
||||
expect(root).toBeTruthy();
|
||||
expect(root.tagName.toLowerCase()).toBe('div');
|
||||
expect(root.getAttribute('data-editor-path')).toBe('1.children.0');
|
||||
expect(root.getAttribute('id')).toBe('fi-root-id');
|
||||
expect(root.querySelector('button')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('숨겨진 file input 에는 편집 표식이 없다', () => {
|
||||
const { container } = render(
|
||||
<FileInput data-editor-name="FileInput" data-editor-path="2.children.1" id="fi-id" />
|
||||
);
|
||||
const hiddenInput = container.querySelector('input[type="file"]') as HTMLElement;
|
||||
expect(hiddenInput).toBeTruthy();
|
||||
expect(hiddenInput.getAttribute('data-editor-name')).toBeNull();
|
||||
expect(hiddenInput.getAttribute('data-editor-path')).toBeNull();
|
||||
expect(hiddenInput.getAttribute('id')).toBeNull();
|
||||
});
|
||||
|
||||
it('기능 prop(name 등)은 file input 에 그대로 전달된다', () => {
|
||||
const { container } = render(
|
||||
<FileInput name="upload_field" data-editor-name="FileInput" />
|
||||
);
|
||||
const hiddenInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
expect(hiddenInput.getAttribute('name')).toBe('upload_field');
|
||||
});
|
||||
});
|
||||
});
|
||||
+45
@@ -431,4 +431,49 @@ describe('PasswordInput 컴포넌트', () => {
|
||||
expect(screen.getByTestId('icon-triangle-exclamation')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// 레이아웃 편집기 표식(data-editor-*)은 내부 input 이 아니라
|
||||
// 시각적 루트(wrapper div)에 부착되어야 캔버스 선택/드래그가 컴포넌트 전체를 잡는다.
|
||||
describe('레이아웃 편집기 표식 부착 위치', () => {
|
||||
it('data-editor-* / onMouseMove 는 wrapper 루트 div 에 부착', () => {
|
||||
const onMouseMove = vi.fn();
|
||||
const { container } = render(
|
||||
<PasswordInput
|
||||
wrapperClassName="vpw-root"
|
||||
{...({
|
||||
'data-editor-id': 'auto_PW_1',
|
||||
'data-editor-name': 'PasswordInput',
|
||||
'data-editor-path': '0.children.1',
|
||||
onMouseMove,
|
||||
} as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
// 루트 div 가 표식을 가져야 함
|
||||
const root = container.firstElementChild as HTMLElement;
|
||||
expect(root.tagName).toBe('DIV');
|
||||
expect(root).toHaveClass('vpw-root');
|
||||
expect(root.getAttribute('data-editor-id')).toBe('auto_PW_1');
|
||||
expect(root.getAttribute('data-editor-name')).toBe('PasswordInput');
|
||||
expect(root.getAttribute('data-editor-path')).toBe('0.children.1');
|
||||
// 내부 input 에는 data-editor-* 가 없어야 함 (루트로만 흘러야 함)
|
||||
const input = container.querySelector('input') as HTMLElement;
|
||||
expect(input.getAttribute('data-editor-path')).toBeNull();
|
||||
expect(input.getAttribute('data-editor-id')).toBeNull();
|
||||
});
|
||||
|
||||
it('일반 input 속성(name/placeholder)은 내부 input 에 그대로 전달', () => {
|
||||
const { container } = render(
|
||||
<PasswordInput
|
||||
name="user_password"
|
||||
placeholder="비밀번호 입력"
|
||||
{...({ 'data-editor-name': 'PasswordInput' } as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
const input = container.querySelector('input') as HTMLInputElement;
|
||||
expect(input.getAttribute('name')).toBe('user_password');
|
||||
expect(input.getAttribute('placeholder')).toBe('비밀번호 입력');
|
||||
// 편집기 표식은 input 에 누출되지 않음
|
||||
expect(input.getAttribute('data-editor-name')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// e2e:allow 편집기 ComponentRegistry 등록 정합(basic Header/Footer export 제거 — composite 와 이름 충돌 해소, SlotContainer 번들 포함). 편집기 캔버스 렌더는 Chrome MCP 라이브 실측 + Vitest(headerEditorSpecAndCurrencySlot/HeaderPropRender)로 검증. 사용자 사이트 런타임 무변경.
|
||||
/**
|
||||
* 기본 HTML 매핑 컴포넌트 Export
|
||||
*
|
||||
@@ -38,8 +39,10 @@ export { Section, type SectionProps } from './Section';
|
||||
export { Svg, type SvgProps } from './Svg';
|
||||
export { Icon, type IconProps } from './Icon';
|
||||
export { Code, type CodeProps } from './Code';
|
||||
export { Footer, type FooterProps } from './Footer';
|
||||
export { Header, type HeaderProps } from './Header';
|
||||
// basic Header/Footer(HTML <header>/<footer> 래퍼)는 레이아웃 사용처 0이며 composite
|
||||
// Header/Footer 와 이름이 겹쳐 편집기 ComponentRegistry 에서 중복 등록 경고를 유발한다.
|
||||
// 사이트 헤더/푸터는 composite 컴포넌트가 담당하므로 basic 래퍼 export 를 제거한다
|
||||
// (필요 시 Div + role/시맨틱 태그로 대체). 파일 자체는 보존(직접 import 경로는 유지).
|
||||
export { Hr, type HrProps } from './Hr';
|
||||
|
||||
// Icon 타입 정의 (enum 및 타입)
|
||||
|
||||
Reference in new issue
Block a user