TDD,E2E 개선, 미적용 스타일 개선
CI / test (push) Failing after 2m54s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-12-07 09:52:42 +09:00
parent e726f43f7c
commit a5b432fb7b
167 changed files with 7397 additions and 663 deletions
+110 -53
View File
@@ -42,6 +42,12 @@ const normalizeTextColor = (raw: unknown): string => {
return trimmed !== "" ? trimmed : "";
};
// Export/Public 레이어용 모서리 둥글기 매핑
// - none: 0px
// - sm: 2px
// - lg: 6px
// - full: 9999px (완전 둥근 모서리)
// - md(기본): 4px
const computeRadiusPx = (radius: unknown): number => {
const token = typeof radius === "string" ? radius : "md";
if (token === "none") return 0;
@@ -51,6 +57,19 @@ const computeRadiusPx = (radius: unknown): number => {
return 4;
};
// Editor 레이어용 모서리 둥글기 매핑
// - none: 0px
// - sm: 4px
// - md: 8px (기본)
// - lg/full: 9999px (pill 스타일)
const computeEditorRadiusPx = (radius: unknown): number => {
const token = typeof radius === "string" ? radius : "md";
if (token === "none") return 0;
if (token === "sm") return 4;
if (token === "lg" || token === "full") return 9999;
return 8;
};
const computeWidthMode = (props: { widthMode?: string; fullWidth?: boolean }): "auto" | "full" | "fixed" => {
if (typeof props.widthMode === "string") {
return props.widthMode as "auto" | "full" | "fixed";
@@ -101,9 +120,68 @@ export const computeFormInputExportTokens = (
inputStyleParts.push(`width:${em}em`);
}
const radiusPx = computeRadiusPx(props.borderRadius);
const radiusToken = typeof props.borderRadius === "string" ? props.borderRadius : "md";
let radiusPx = 4;
if (radiusToken === "none") {
radiusPx = 0;
} else if (radiusToken === "sm") {
radiusPx = 2;
} else if (radiusToken === "lg") {
radiusPx = 6;
} else if (radiusToken === "full") {
radiusPx = 9999;
}
inputStyleParts.push(`border-radius:${radiusPx}px`);
// 폰트 관련 커스텀 값: px 단위일 경우 em 으로 변환하고, 그렇지 않으면 원문 값을 그대로 사용한다.
if (typeof props.fontSizeCustom === "string" && props.fontSizeCustom.trim() !== "") {
const raw = props.fontSizeCustom.trim();
if (raw.endsWith("px")) {
const match = raw.match(/-?\d+(?:\.\d+)?/);
if (match) {
const px = parseFloat(match[0]);
if (Number.isFinite(px)) {
const em = px / 16;
inputStyleParts.push(`font-size:${em}em`);
}
}
} else {
inputStyleParts.push(`font-size:${escapeAttr(raw)}`);
}
}
if (typeof props.lineHeightCustom === "string" && props.lineHeightCustom.trim() !== "") {
const raw = props.lineHeightCustom.trim();
if (raw.endsWith("px")) {
const match = raw.match(/-?\d+(?:\.\d+)?/);
if (match) {
const px = parseFloat(match[0]);
if (Number.isFinite(px)) {
const em = px / 16;
inputStyleParts.push(`line-height:${em}em`);
}
}
} else {
inputStyleParts.push(`line-height:${escapeAttr(raw)}`);
}
}
if (typeof props.letterSpacingCustom === "string" && props.letterSpacingCustom.trim() !== "") {
const raw = props.letterSpacingCustom.trim();
if (raw.endsWith("px")) {
const match = raw.match(/-?\d+(?:\.\d+)?/);
if (match) {
const px = parseFloat(match[0]);
if (Number.isFinite(px)) {
const em = px / 16;
inputStyleParts.push(`letter-spacing:${em}em`);
}
}
} else {
inputStyleParts.push(`letter-spacing:${escapeAttr(raw)}`);
}
}
const inputStyleAttr = inputStyleParts.length > 0 ? ` style="${inputStyleParts.join(";")}"` : "";
return {
@@ -254,14 +332,9 @@ export const computeFormInputEditorTokens = (props: FormInputBlockProps): FormIn
fieldStyle.width = `${props.widthPx}px`;
}
const radius = props.borderRadius ?? "md";
if (radius === "none") {
fieldStyle.borderRadius = 0;
} else if (radius === "sm") {
fieldStyle.borderRadius = 4;
} else if (radius === "lg" || radius === "full") {
fieldStyle.borderRadius = 9999;
}
// 모서리 둥글기: Editor 레이어에서는 lg/full 토큰을 pill 형태(9999px)로 사용한다.
const radiusPx = computeEditorRadiusPx(props.borderRadius);
fieldStyle.borderRadius = radiusPx;
// 에디터에서는 px 기반 padding/타이포 값을 그대로 fieldStyle 에 반영해 미리보기에서 변화가 보이도록 한다.
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
@@ -336,14 +409,9 @@ export const computeFormSelectEditorTokens = (props: FormSelectBlockProps): Form
fieldStyle.width = `${props.widthPx}px`;
}
const radius = props.borderRadius ?? "md";
if (radius === "none") {
fieldStyle.borderRadius = 0;
} else if (radius === "sm") {
fieldStyle.borderRadius = 4;
} else if (radius === "lg" || radius === "full") {
fieldStyle.borderRadius = 9999;
}
// 모서리 둥글기: Editor 레이어에서는 lg/full 토큰을 pill 형태(9999px)로 사용한다.
const selectRadiusPx = computeEditorRadiusPx(props.borderRadius);
fieldStyle.borderRadius = selectRadiusPx;
// 체크박스/라디오 그룹도 에디터에서 padding/타이포 스타일을 일부 반영해준다.
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
@@ -389,14 +457,9 @@ export const computeFormOptionGroupEditorTokens = (
fieldStyle.width = `${props.widthPx}px`;
}
const radius = props.borderRadius ?? "md";
if (radius === "none") {
fieldStyle.borderRadius = 0;
} else if (radius === "sm") {
fieldStyle.borderRadius = 4;
} else if (radius === "lg" || radius === "full") {
fieldStyle.borderRadius = 9999;
}
// 모서리 둥글기: Editor 레이어에서는 lg/full 토큰을 pill 형태(9999px)로 사용한다.
const groupRadiusPx = computeEditorRadiusPx(props.borderRadius);
fieldStyle.borderRadius = groupRadiusPx;
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
fieldStyle.paddingInline = `${props.paddingX}px`;
@@ -535,23 +598,18 @@ export const computeFormInputPublicTokens = (props: FormInputBlockProps): FormIn
}
}
// 색상 관련 커스텀 값
// 색상 관련 커스텀 값: builder.css 의 pb-input 기본 색상을 덮어쓰지 않도록,
// 사용자가 커스텀 값을 설정했을 때에만 인라인 스타일을 적용한다.
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
inputStyle.color = props.textColorCustom.trim();
} else {
inputStyle.color = "#f9fafb";
}
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
inputStyle.backgroundColor = props.fillColorCustom.trim();
} else {
inputStyle.backgroundColor = "#020617";
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
inputStyle.borderColor = props.strokeColorCustom.trim();
} else {
inputStyle.borderColor = "#334155";
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
@@ -629,14 +687,12 @@ export const computeFormSelectPublicTokens = (props: FormSelectBlockProps): Form
}
}
// 텍스트 색상: textColorCustom 이 설정되어 있으면 해당 색상을 사용하고, 없으면 기본 밝은 텍스트 색상을 사용한다.
// 텍스트 색상: textColorCustom 이 설정되어 있으면 해당 색상을 사용하고,
// 없으면 builder.css 의 pb-select 기본 색상을 그대로 사용한다.
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
const colorValue = props.textColorCustom.trim();
wrapperStyle.color = colorValue;
selectStyle.color = colorValue;
} else {
wrapperStyle.color = "#f9fafb";
selectStyle.color = "#f9fafb";
}
// 배경/테두리 색상: fillColorCustom/strokeColorCustom 이 있으면 select 에 적용한다.
@@ -694,6 +750,7 @@ const computeOptionGroupPublicTokensBase = (
const textSizeClass = props.fontSizeCustom && props.fontSizeCustom.trim() !== "" ? "" : "text-xs";
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
// 퍼블릭 토큰에서는 고정 너비를 em 단위로 변환해 px 단위를 피한다.
groupStyle.width = pxToEm(props.widthPx);
}
@@ -783,7 +840,12 @@ const computeOptionGroupPublicTokensBase = (
optionContainerStyle.borderRadius = `${radiusPx}px`;
if (typeof props.optionGapPx === "number" && props.optionGapPx >= 0) {
optionsStyle.rowGap = pxToEm(props.optionGapPx);
const gapPx = props.optionGapPx;
const gapEm = pxToEm(gapPx);
optionsStyle.rowGap = gapEm;
if ((props as any).optionLayout === "inline") {
optionsStyle.columnGap = gapEm;
}
}
return {
@@ -844,6 +906,10 @@ export const computeFormControllerPublicTokens = (
.map((fieldBlock) => {
const anyProps: any = fieldBlock.props ?? {};
const isRequired = Array.isArray(props.requiredFieldIds)
? props.requiredFieldIds.includes(fieldBlock.id)
: false;
if (fieldBlock.type === "formInput") {
const name = anyProps.formFieldName ?? fieldBlock.id;
const label = anyProps.label ?? anyProps.formFieldName ?? "입력 필드";
@@ -854,7 +920,7 @@ export const computeFormControllerPublicTokens = (
label,
// 기존 PublicPageRenderer 컨트롤러 로직과 동일하게 formInput 은 항상 type "text" 로 취급한다.
type: "text",
required: Boolean(anyProps.required),
required: isRequired,
} satisfies FormControllerFieldPublicConfig;
}
@@ -869,7 +935,7 @@ export const computeFormControllerPublicTokens = (
label,
type: "select",
options,
required: Boolean(anyProps.required),
required: isRequired,
} satisfies FormControllerFieldPublicConfig;
}
@@ -886,7 +952,7 @@ export const computeFormControllerPublicTokens = (
options,
groupLabelMode: anyProps.groupLabelMode,
groupLabelImageUrl: anyProps.groupLabelImageUrl,
required: Boolean(anyProps.required),
required: isRequired,
} satisfies FormControllerFieldPublicConfig;
}
@@ -902,23 +968,14 @@ export const computeFormControllerPublicTokens = (
options,
groupLabelMode: anyProps.groupLabelMode,
groupLabelImageUrl: anyProps.groupLabelImageUrl,
required: Boolean(anyProps.required),
required: isRequired,
} satisfies FormControllerFieldPublicConfig;
})
: [];
const fields: FormControllerFieldPublicConfig[] =
hasControllerFields && controllerFields.length > 0
? controllerFields
: Array.isArray(props.fields) && props.fields.length > 0
? props.fields.map((field) => ({
id: field.id,
name: field.name,
label: field.label,
type: field.type,
required: field.required,
}))
: [];
// 프리뷰/퍼블릭 렌더러에서는 FormBlock 의 v1 fields 설정을 사용하지 않고,
// fieldIds 로 연결된 실제 폼 필드 블록들만 기반으로 폼 필드를 렌더링한다.
const fields: FormControllerFieldPublicConfig[] = controllerFields;
const mappedSubmitButton = blocks.find(
(b) => b.type === "button" && b.id === props.submitButtonId,