1차 싱크 완료
CI / test (push) Failing after 41m13s
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-11-24 21:32:37 +09:00
parent 7a8ad7c057
commit 672cca5271
83 changed files with 6681 additions and 325 deletions
@@ -123,6 +123,11 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
textStyle.color = props.colorCustom;
}
// 블록 배경색: backgroundColorCustom 이 설정된 경우에만 적용한다 (기본은 투명/미지정).
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
textStyle.backgroundColor = props.backgroundColorCustom.trim();
}
return (
<p
key={block.id}
@@ -314,6 +319,41 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
}
}
// 텍스트 색상: textColorCustom 이 설정되어 있으면 해당 색상을 사용하고, 없으면 기본 밝은 텍스트 색상을 사용한다.
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 에 적용한다.
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
const bgValue = props.fillColorCustom.trim();
selectStyle.backgroundColor = bgValue;
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
const strokeValue = props.strokeColorCustom.trim();
selectStyle.borderColor = strokeValue;
}
// 모서리 둥글기: borderRadius 토큰을 px 로 변환한다.
const selectRadiusToken = props.borderRadius ?? "md";
const selectRadiusPx =
selectRadiusToken === "none"
? 0
: selectRadiusToken === "sm"
? 2
: selectRadiusToken === "lg"
? 6
: selectRadiusToken === "full"
? 9999
: 4;
selectStyle.borderRadius = `${selectRadiusPx}px`;
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
const paddingEm = pxToEm(props.paddingX);
selectStyle.paddingInline = paddingEm;
@@ -353,6 +393,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
const widthMode = props.widthMode ?? (props.fullWidth ? "full" : "auto");
const widthClass = widthMode === "full" ? "w-full" : "";
const groupStyle: CSSProperties = {};
const optionContainerStyle: CSSProperties = {};
const optionTextStyle: CSSProperties = {};
const groupTextStyle: CSSProperties = {};
const textSizeClass = props.fontSizeCustom && props.fontSizeCustom.trim() !== "" ? "" : "text-xs";
@@ -404,14 +445,51 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
}
}
// 텍스트 색상: textColorCustom 이 설정되어 있으면 그룹/옵션 텍스트 모두에 동일하게 적용한다.
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
const colorValue = props.textColorCustom.trim();
groupTextStyle.color = colorValue;
optionTextStyle.color = colorValue;
} else {
groupTextStyle.color = "#e5e7eb";
optionTextStyle.color = "#e5e7eb";
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
optionTextStyle.paddingInline = pxToEm(props.paddingX);
optionContainerStyle.paddingInline = pxToEm(props.paddingX);
}
if (typeof props.paddingY === "number" && props.paddingY >= 0) {
optionTextStyle.paddingBlock = pxToEm(props.paddingY);
optionContainerStyle.paddingBlock = pxToEm(props.paddingY);
}
// 옵션 컨테이너 배경/테두리 색상
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
const bgValue = props.fillColorCustom.trim();
optionContainerStyle.backgroundColor = bgValue;
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
const strokeValue = props.strokeColorCustom.trim();
optionContainerStyle.borderColor = strokeValue;
optionContainerStyle.borderWidth = "1px";
optionContainerStyle.borderStyle = "solid";
}
// 옵션 컨테이너 모서리 둥글기
const checkboxRadiusToken = props.borderRadius ?? "md";
const checkboxRadiusPx =
checkboxRadiusToken === "none"
? 0
: checkboxRadiusToken === "sm"
? 2
: checkboxRadiusToken === "lg"
? 6
: checkboxRadiusToken === "full"
? 9999
: 4;
optionContainerStyle.borderRadius = `${checkboxRadiusPx}px`;
const optionsStyle: CSSProperties = {};
if (typeof props.optionGapPx === "number" && props.optionGapPx >= 0) {
optionsStyle.rowGap = pxToEm(props.optionGapPx);
@@ -439,7 +517,11 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
)}
<div className="flex flex-col" data-testid="preview-form-checkbox-options" style={optionsStyle}>
{props.options.map((opt) => (
<label key={opt.value} className="inline-flex items-center gap-1">
<label
key={opt.value}
className="inline-flex items-center gap-1"
style={optionContainerStyle}
>
<input
type="checkbox"
className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-sky-500"
@@ -469,6 +551,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
const widthMode = props.widthMode ?? (props.fullWidth ? "full" : "auto");
const widthClass = widthMode === "full" ? "w-full" : "";
const groupStyle: CSSProperties = {};
const optionContainerStyle: CSSProperties = {};
const optionTextStyle: CSSProperties = {};
const groupTextStyle: CSSProperties = {};
const textSizeClass = props.fontSizeCustom && props.fontSizeCustom.trim() !== "" ? "" : "text-xs";
@@ -520,14 +603,51 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
}
}
// 텍스트 색상: textColorCustom 이 설정되어 있으면 그룹/옵션 텍스트 모두에 동일하게 적용한다.
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
const colorValue = props.textColorCustom.trim();
groupTextStyle.color = colorValue;
optionTextStyle.color = colorValue;
} else {
groupTextStyle.color = "#e5e7eb";
optionTextStyle.color = "#e5e7eb";
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
optionTextStyle.paddingInline = pxToEm(props.paddingX);
optionContainerStyle.paddingInline = pxToEm(props.paddingX);
}
if (typeof props.paddingY === "number" && props.paddingY >= 0) {
optionTextStyle.paddingBlock = pxToEm(props.paddingY);
optionContainerStyle.paddingBlock = pxToEm(props.paddingY);
}
// 옵션 컨테이너 배경/테두리 색상
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
const bgValue = props.fillColorCustom.trim();
optionContainerStyle.backgroundColor = bgValue;
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
const strokeValue = props.strokeColorCustom.trim();
optionContainerStyle.borderColor = strokeValue;
optionContainerStyle.borderWidth = "1px";
optionContainerStyle.borderStyle = "solid";
}
// 옵션 컨테이너 모서리 둥글기
const radioRadiusToken = props.borderRadius ?? "md";
const radioRadiusPx =
radioRadiusToken === "none"
? 0
: radioRadiusToken === "sm"
? 2
: radioRadiusToken === "lg"
? 6
: radioRadiusToken === "full"
? 9999
: 4;
optionContainerStyle.borderRadius = `${radioRadiusPx}px`;
const optionsStyle: CSSProperties = {};
if (typeof props.optionGapPx === "number" && props.optionGapPx >= 0) {
optionsStyle.rowGap = pxToEm(props.optionGapPx);
@@ -555,7 +675,11 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
)}
<div className="flex flex-col" data-testid="preview-form-radio-options" style={optionsStyle}>
{props.options.map((opt) => (
<label key={opt.value} className="inline-flex items-center gap-1">
<label
key={opt.value}
className="inline-flex items-center gap-1"
style={optionContainerStyle}
>
<input
type="radio"
className="h-4 w-4 border-slate-700 bg-slate-900 text-sky-500"
@@ -753,6 +877,11 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
listStyle.color = props.textColorCustom;
}
// 리스트 배경색: backgroundColorCustom 이 설정된 경우에만 적용한다.
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
listStyle.backgroundColor = props.backgroundColorCustom.trim();
}
const bulletStyle = bulletStyleRaw;
const itemsTree: ListItemNode[] =
@@ -922,6 +1051,11 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
formStyle.marginBottom = marginEm;
}
// 폼 배경색: backgroundColorCustom 이 설정된 경우에만 적용한다.
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
formStyle.backgroundColor = props.backgroundColorCustom.trim();
}
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
@@ -1097,6 +1231,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
props.align === "left" ? "justify-start" : props.align === "right" ? "justify-end" : "justify-center";
const widthMode = props.widthMode ?? "auto";
const wrapperStyle: CSSProperties = {};
const imageStyle: CSSProperties = {
display: "block",
maxWidth: "100%",
@@ -1124,8 +1259,12 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
: fallbackRadiusPx;
imageStyle.borderRadius = radiusPx === 9999 ? "9999px" : pxToEm(radiusPx);
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
wrapperStyle.backgroundColor = props.backgroundColorCustom;
}
return (
<div key={block.id} className={`w-full flex ${alignClass}`}>
<div key={block.id} className={`w-full flex ${alignClass}`} style={wrapperStyle}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={props.src || ""} alt={props.alt} className="object-contain" style={imageStyle} />
</div>
@@ -1200,10 +1339,10 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
};
return (
<div className="flex-1 flex flex-col bg-slate-950 text-slate-50">
<div className="flex-1 flex flex-col text-slate-50">
{/* 루트 텍스트/버튼/이미지 블록들이 있다면 페이지 상단에 노출 */}
{rootBlocks.length > 0 && (
<section className="bg-slate-950 py-12">
<section className="py-12">
<div className="mx-auto max-w-3xl px-4 flex flex-col gap-4">
{rootBlocks.map((b) => (
<div key={b.id}>{renderBlock(b)}</div>