[agent-pipeline] pipe-20260329160157-3bqb round-2

This commit is contained in:
DDD1542
2026-03-30 02:39:43 +09:00
parent 7529c3ff9e
commit a9d3a526af
19 changed files with 268 additions and 426 deletions
@@ -244,9 +244,9 @@ function BarcodePreview({
component.qrUseMultiField &&
component.qrDataFields &&
component.qrDataFields.length > 0 &&
component.queryId
component.query_id
) {
const queryResult = getQueryResult(component.queryId);
const queryResult = getQueryResult(component.query_id);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
if (component.qrIncludeAllRows) {
const allRowsData: Record<string, string>[] = [];
@@ -275,8 +275,8 @@ function BarcodePreview({
}
// 단일 필드 바인딩
if (component.barcodeFieldName && component.queryId) {
const queryResult = getQueryResult(component.queryId);
if (component.barcodeFieldName && component.query_id) {
const queryResult = getQueryResult(component.query_id);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
if (isQR && component.qrIncludeAllRows) {
const allValues = queryResult.rows
@@ -367,14 +367,14 @@ function BarcodePreview({
}
export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) {
const { layoutConfig, getQueryResult, reportDetail } = useReportDesigner();
const { layout_config: layoutConfig, getQueryResult, report_detail: reportDetail } = useReportDesigner();
const [isExporting, setIsExporting] = useState(false);
const { toast } = useToast();
// 컴포넌트의 실제 표시 값 가져오기
const getComponentValue = (component: any): string => {
if (component.queryId && component.fieldName) {
const queryResult = getQueryResult(component.queryId);
if (component.query_id && component.fieldName) {
const queryResult = getQueryResult(component.query_id);
if (queryResult && queryResult.rows.length > 0) {
const value = queryResult.rows[0][component.fieldName];
if (value !== null && value !== undefined) {
@@ -398,9 +398,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
component.qrUseMultiField &&
component.qrDataFields &&
component.qrDataFields.length > 0 &&
component.queryId
component.query_id
) {
const queryResult = getQueryResult(component.queryId);
const queryResult = getQueryResult(component.query_id);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
if (component.qrIncludeAllRows) {
const allRowsData: Record<string, string>[] = [];
@@ -428,8 +428,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
}
}
if (component.barcodeFieldName && component.queryId) {
const queryResult = getQueryResult(component.queryId);
if (component.barcodeFieldName && component.query_id) {
const queryResult = getQueryResult(component.query_id);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
if (isQR && component.qrIncludeAllRows) {
const allValues = queryResult.rows
@@ -586,39 +586,39 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
): string => {
const componentsHTML = pageComponents
.map((component) => {
const queryResult = component.queryId ? getQueryResult(component.queryId) : null;
const queryResult = component.query_id ? getQueryResult(component.query_id) : null;
let content = "";
// Text/Label 컴포넌트
if (component.type === "text" || component.type === "label") {
const displayValue = getComponentValue(component);
content = `<div style="font-size: ${component.fontSize || 13}px; color: ${component.fontColor || "#000000"}; font-weight: ${component.fontWeight || "normal"}; text-align: ${component.textAlign || "left"}; white-space: pre-wrap;">${displayValue}</div>`;
content = `<div style="font-size: ${component.font_size || 13}px; color: ${component.font_color || "#000000"}; font-weight: ${component.font_weight || "normal"}; text-align: ${component.text_align || "left"}; white-space: pre-wrap;">${displayValue}</div>`;
}
// Image 컴포넌트
else if (component.type === "image" && component.imageUrl) {
const imageUrl = component.imageUrl.startsWith("data:")
? component.imageUrl
: getFullImageUrl(component.imageUrl);
content = `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.objectFit || "contain"};" />`;
else if (component.type === "image" && component.image_url) {
const imageUrl = component.image_url.startsWith("data:")
? component.image_url
: getFullImageUrl(component.image_url);
content = `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.object_fit || "contain"};" />`;
}
// Divider 컴포넌트
else if (component.type === "divider") {
const width = component.orientation === "horizontal" ? "100%" : `${component.lineWidth || 1}px`;
const height = component.orientation === "vertical" ? "100%" : `${component.lineWidth || 1}px`;
content = `<div style="width: ${width}; height: ${height}; background-color: ${component.lineColor || "#000000"};"></div>`;
const width = component.orientation === "horizontal" ? "100%" : `${component.line_width || 1}px`;
const height = component.orientation === "vertical" ? "100%" : `${component.line_width || 1}px`;
content = `<div style="width: ${width}; height: ${height}; background-color: ${component.line_color || "#000000"};"></div>`;
}
// Signature 컴포넌트
else if (component.type === "signature") {
const labelPosition = component.labelPosition || "left";
const showLabel = component.showLabel !== false;
const labelText = component.labelText || "서명:";
const imageUrl = component.imageUrl
? component.imageUrl.startsWith("data:")
? component.imageUrl
: getFullImageUrl(component.imageUrl)
const labelPosition = component.label_position || "left";
const showLabel = component.show_label !== false;
const labelText = component.label_text || "서명:";
const imageUrl = component.image_url
? component.image_url.startsWith("data:")
? component.image_url
: getFullImageUrl(component.image_url)
: "";
if (labelPosition === "left" || labelPosition === "right") {
@@ -626,7 +626,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<div style="display: flex; align-items: center; flex-direction: ${labelPosition === "right" ? "row-reverse" : "row"}; gap: 8px; height: 100%;">
${showLabel ? `<div style="font-size: 12px; white-space: nowrap;">${labelText}</div>` : ""}
<div style="flex: 1; position: relative;">
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.objectFit || "contain"};" />` : ""}
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.object_fit || "contain"};" />` : ""}
</div>
</div>`;
} else {
@@ -634,7 +634,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<div style="display: flex; flex-direction: column; align-items: center; height: 100%;">
${showLabel && labelPosition === "top" ? `<div style="font-size: 12px;">${labelText}</div>` : ""}
<div style="flex: 1; width: 100%; position: relative;">
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.objectFit || "contain"};" />` : ""}
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.object_fit || "contain"};" />` : ""}
</div>
${showLabel && labelPosition === "bottom" ? `<div style="font-size: 12px;">${labelText}</div>` : ""}
</div>`;
@@ -643,20 +643,20 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
// Stamp 컴포넌트
else if (component.type === "stamp") {
const showLabel = component.showLabel !== false;
const labelText = component.labelText || "(인)";
const personName = component.personName || "";
const imageUrl = component.imageUrl
? component.imageUrl.startsWith("data:")
? component.imageUrl
: getFullImageUrl(component.imageUrl)
const showLabel = component.show_label !== false;
const labelText = component.label_text || "(인)";
const personName = component.person_name || "";
const imageUrl = component.image_url
? component.image_url.startsWith("data:")
? component.image_url
: getFullImageUrl(component.image_url)
: "";
content = `
<div style="display: flex; align-items: center; gap: 8px; width: 100%; height: 100%;">
${personName ? `<div style="font-size: 12px; white-space: nowrap;">${personName}</div>` : ""}
<div style="position: relative; flex: 1; height: 100%;">
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.objectFit || "contain"}; border-radius: 50%;" />` : ""}
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.object_fit || "contain"}; border-radius: 50%;" />` : ""}
${showLabel ? `<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; font-weight: bold; color: #dc2626;">${labelText}</div>` : ""}
</div>
</div>`;
@@ -664,7 +664,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
// PageNumber 컴포넌트
else if (component.type === "pageNumber") {
const format = component.pageNumberFormat || "number";
const format = component.page_number_format || "number";
let pageNumberText = "";
switch (format) {
case "number":
@@ -679,22 +679,22 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
default:
pageNumberText = `${pageIndex + 1}`;
}
content = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; font-size: ${component.fontSize}px; color: ${component.fontColor}; font-weight: ${component.fontWeight}; text-align: ${component.textAlign};">${pageNumberText}</div>`;
content = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; font-size: ${component.font_size}px; color: ${component.font_color}; font-weight: ${component.font_weight}; text-align: ${component.text_align};">${pageNumberText}</div>`;
}
// Card 컴포넌트
else if (component.type === "card") {
const cardTitle = component.cardTitle || "정보 카드";
const cardItems = component.cardItems || [];
const labelWidth = component.labelWidth || 80;
const showCardTitle = component.showCardTitle !== false;
const titleFontSize = component.titleFontSize || 14;
const labelFontSize = component.labelFontSize || 13;
const valueFontSize = component.valueFontSize || 13;
const titleColor = component.titleColor || "#1e40af";
const labelColor = component.labelColor || "#374151";
const valueColor = component.valueColor || "#000000";
const borderColor = component.borderColor || "#e5e7eb";
const cardTitle = component.card_title || "정보 카드";
const cardItems = component.card_items || [];
const labelWidth = component.label_width || 80;
const showCardTitle = component.show_card_title !== false;
const titleFontSize = component.title_font_size || 14;
const labelFontSize = component.label_font_size || 13;
const valueFontSize = component.value_font_size || 13;
const titleColor = component.title_color || "#1e40af";
const labelColor = component.label_color || "#374151";
const valueColor = component.value_color || "#000000";
const borderColor = component.border_color || "#e5e7eb";
// 쿼리 바인딩된 값 가져오기
const getCardValue = (item: { label: string; value: string; fieldName?: string }) => {
@@ -736,18 +736,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
// 계산 컴포넌트
else if (component.type === "calculation") {
const calcItems = component.calcItems || [];
const resultLabel = component.resultLabel || "합계";
const calcLabelWidth = component.labelWidth || 120;
const calcLabelFontSize = component.labelFontSize || 13;
const calcValueFontSize = component.valueFontSize || 13;
const calcResultFontSize = component.resultFontSize || 16;
const calcLabelColor = component.labelColor || "#374151";
const calcValueColor = component.valueColor || "#000000";
const calcResultColor = component.resultColor || "#2563eb";
const numberFormat = component.numberFormat || "currency";
const currencySuffix = component.currencySuffix || "원";
const borderColor = component.borderColor || "#374151";
const calcItems = component.calc_items || [];
const resultLabel = component.result_label || "합계";
const calcLabelWidth = component.label_width || 120;
const calcLabelFontSize = component.label_font_size || 13;
const calcValueFontSize = component.value_font_size || 13;
const calcResultFontSize = component.result_font_size || 16;
const calcLabelColor = component.label_color || "#374151";
const calcValueColor = component.value_color || "#000000";
const calcResultColor = component.result_color || "#2563eb";
const numberFormat = component.number_format || "currency";
const currencySuffix = component.currency_suffix || "원";
const borderColor = component.border_color || "#374151";
// 숫자 포맷팅 함수
const formatNumber = (num: number): string => {
@@ -832,16 +832,16 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
// 체크박스 컴포넌트 (인쇄용)
else if (component.type === "checkbox") {
const checkboxSize = component.checkboxSize || 18;
const checkboxColor = component.checkboxColor || "#2563eb";
const checkboxBorderColor = component.checkboxBorderColor || "#6b7280";
const checkboxLabel = component.checkboxLabel || "";
const checkboxLabelPosition = component.checkboxLabelPosition || "right";
const checkboxSize = component.checkbox_size || 18;
const checkboxColor = component.checkbox_color || "#2563eb";
const checkboxBorderColor = component.checkbox_border_color || "#6b7280";
const checkboxLabel = component.checkbox_label || "";
const checkboxLabelPosition = component.checkbox_label_position || "right";
// 체크 상태 결정
let isChecked = component.checkboxChecked === true;
if (component.checkboxFieldName && queryResult && queryResult.rows && queryResult.rows.length > 0) {
const val = queryResult.rows[0][component.checkboxFieldName];
let isChecked = component.checkbox_checked === true;
if (component.checkbox_field_name && queryResult && queryResult.rows && queryResult.rows.length > 0) {
const val = queryResult.rows[0][component.checkbox_field_name];
isChecked = val === true || val === "Y" || val === "1" || val === 1 || val === "true";
}
@@ -862,8 +862,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
// Table 컴포넌트
else if (component.type === "table" && queryResult && queryResult.rows.length > 0) {
const columns =
component.tableColumns && component.tableColumns.length > 0
? component.tableColumns
component.table_columns && component.table_columns.length > 0
? component.table_columns
: queryResult.fields.map((field) => ({
field,
header: field,
@@ -875,17 +875,17 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
.map(
(row) => `
<tr>
${columns.map((col: { field: string; align?: string }) => `<td style="border: ${component.showBorder !== false ? "1px solid #d1d5db" : "none"}; padding: 6px 8px; text-align: ${col.align || "left"}; height: ${component.rowHeight || "auto"}px;">${String(row[col.field] ?? "")}</td>`).join("")}
${columns.map((col: { field: string; align?: string }) => `<td style="border: ${component.show_border !== false ? "1px solid #d1d5db" : "none"}; padding: 6px 8px; text-align: ${col.align || "left"}; height: ${component.row_height || "auto"}px;">${String(row[col.field] ?? "")}</td>`).join("")}
</tr>
`,
)
.join("");
content = `
<table style="width: 100%; border-collapse: ${component.showBorder !== false ? "collapse" : "separate"}; font-size: 12px;">
<table style="width: 100%; border-collapse: ${component.show_border !== false ? "collapse" : "separate"}; font-size: 12px;">
<thead style="display: table-header-group; break-inside: avoid; break-after: avoid;">
<tr style="background-color: ${component.headerBackgroundColor || "#f3f4f6"}; color: ${component.headerTextColor || "#111827"};">
${columns.map((col: { header: string; align?: string; width?: number }) => `<th style="border: ${component.showBorder !== false ? "1px solid #d1d5db" : "none"}; padding: 6px 8px; text-align: ${col.align || "left"}; width: ${col.width ? `${col.width}px` : "auto"}; font-weight: 600;">${col.header}</th>`).join("")}
<tr style="background-color: ${component.header_background_color || "#f3f4f6"}; color: ${component.header_text_color || "#111827"};">
${columns.map((col: { header: string; align?: string; width?: number }) => `<th style="border: ${component.show_border !== false ? "1px solid #d1d5db" : "none"}; padding: 6px 8px; text-align: ${col.align || "left"}; width: ${col.width ? `${col.width}px` : "auto"}; font-weight: 600;">${col.header}</th>`).join("")}
</tr>
</thead>
<tbody>
@@ -902,7 +902,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
const heightMm = component.height / MM_TO_PX;
return `
<div style="position: absolute; left: ${xMm}mm; top: ${yMm}mm; width: ${widthMm}mm; height: ${heightMm}mm; background-color: ${component.backgroundColor || "transparent"}; border: ${component.borderWidth ? `${component.borderWidth}px solid ${component.borderColor}` : "none"}; box-sizing: border-box; overflow: hidden;">
<div style="position: absolute; left: ${xMm}mm; top: ${yMm}mm; width: ${widthMm}mm; height: ${heightMm}mm; background-color: ${component.background_color || "transparent"}; border: ${component.border_width ? `${component.border_width}px solid ${component.border_color}` : "none"}; box-sizing: border-box; overflow: hidden;">
${content}
</div>`;
})
@@ -1064,9 +1064,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
const componentsWithBase64 = await Promise.all(
(Array.isArray(page.components) ? page.components : []).map(async (component) => {
// 이미지가 있는 컴포넌트는 Base64로 변환
if (component.imageUrl) {
if (component.image_url) {
try {
const base64 = await imageUrlToBase64(component.imageUrl);
const base64 = await imageUrlToBase64(component.image_url);
return { ...component, imageBase64: base64 };
} catch {
return component;
@@ -1093,10 +1093,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
for (const page of layoutConfig.pages) {
const pageComponents = Array.isArray(page.components) ? page.components : [];
for (const component of pageComponents) {
if (component.queryId) {
const result = getQueryResult(component.queryId);
if (component.query_id) {
const result = getQueryResult(component.query_id);
if (result) {
queryResults[component.queryId] = result;
queryResults[component.query_id] = result;
}
}
}
@@ -1182,7 +1182,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
)}
{(Array.isArray(page.components) ? page.components : []).map((component) => {
const displayValue = getComponentValue(component);
const queryResult = component.queryId ? getQueryResult(component.queryId) : null;
const queryResult = component.query_id ? getQueryResult(component.query_id) : null;
return (
<div
@@ -1193,9 +1193,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
top: `${component.y}px`,
width: `${component.width}px`,
height: `${component.height}px`,
backgroundColor: component.backgroundColor,
border: component.borderWidth
? `${component.borderWidth}px solid ${component.borderColor}`
backgroundColor: component.background_color,
border: component.border_width
? `${component.border_width}px solid ${component.border_color}`
: "none",
padding: "8px",
}}
@@ -1203,10 +1203,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
{component.type === "text" && (
<div
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
fontSize: `${component.font_size}px`,
color: component.font_color,
fontWeight: component.font_weight,
textAlign: component.text_align as "left" | "center" | "right",
whiteSpace: "pre-wrap",
}}
>
@@ -1217,10 +1217,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
{component.type === "label" && (
<div
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
fontSize: `${component.font_size}px`,
color: component.font_color,
fontWeight: component.font_weight,
textAlign: component.text_align as "left" | "center" | "right",
whiteSpace: "pre-wrap",
}}
>
@@ -1232,8 +1232,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
(() => {
// tableColumns가 없으면 자동 생성
const columns =
component.tableColumns && component.tableColumns.length > 0
? component.tableColumns
component.table_columns && component.table_columns.length > 0
? component.table_columns
: queryResult.fields.map((field) => ({
field,
header: field,
@@ -1245,22 +1245,22 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<table
style={{
width: "100%",
borderCollapse: component.showBorder !== false ? "collapse" : "separate",
borderCollapse: component.show_border !== false ? "collapse" : "separate",
fontSize: "12px",
}}
>
<thead>
<tr
style={{
backgroundColor: component.headerBackgroundColor || "#f3f4f6",
color: component.headerTextColor || "#111827",
backgroundColor: component.header_background_color || "#f3f4f6",
color: component.header_text_color || "#111827",
}}
>
{columns.map((col) => (
<th
key={col.field}
style={{
border: component.showBorder !== false ? "1px solid #d1d5db" : "none",
border: component.show_border !== false ? "1px solid #d1d5db" : "none",
padding: "6px 8px",
textAlign: col.align || "left",
width: col.width ? `${col.width}px` : "auto",
@@ -1279,10 +1279,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<td
key={col.field}
style={{
border: component.showBorder !== false ? "1px solid #d1d5db" : "none",
border: component.show_border !== false ? "1px solid #d1d5db" : "none",
padding: "6px 8px",
textAlign: col.align || "left",
height: component.rowHeight ? `${component.rowHeight}px` : "auto",
height: component.row_height ? `${component.row_height}px` : "auto",
}}
>
{String(row[col.field] ?? "")}
@@ -1298,14 +1298,14 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<div className="text-xs text-muted-foreground/70"> </div>
) : null}
{component.type === "image" && component.imageUrl && (
{component.type === "image" && component.image_url && (
<img
src={getFullImageUrl(component.imageUrl)}
src={getFullImageUrl(component.image_url)}
alt="이미지"
style={{
width: "100%",
height: "100%",
objectFit: component.objectFit || "contain",
objectFit: component.object_fit || "contain",
}}
/>
)}
@@ -1314,34 +1314,34 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<div
style={{
width:
component.orientation === "horizontal" ? "100%" : `${component.lineWidth || 1}px`,
height: component.orientation === "vertical" ? "100%" : `${component.lineWidth || 1}px`,
backgroundColor: component.lineColor || "#000000",
...(component.lineStyle === "dashed" && {
component.orientation === "horizontal" ? "100%" : `${component.line_width || 1}px`,
height: component.orientation === "vertical" ? "100%" : `${component.line_width || 1}px`,
backgroundColor: component.line_color || "#000000",
...(component.line_style === "dashed" && {
backgroundImage: `repeating-linear-gradient(
${component.orientation === "horizontal" ? "90deg" : "0deg"},
${component.lineColor || "#000000"} 0px,
${component.lineColor || "#000000"} 10px,
${component.line_color || "#000000"} 0px,
${component.line_color || "#000000"} 10px,
transparent 10px,
transparent 20px
)`,
backgroundColor: "transparent",
}),
...(component.lineStyle === "dotted" && {
...(component.line_style === "dotted" && {
backgroundImage: `repeating-linear-gradient(
${component.orientation === "horizontal" ? "90deg" : "0deg"},
${component.lineColor || "#000000"} 0px,
${component.lineColor || "#000000"} 3px,
${component.line_color || "#000000"} 0px,
${component.line_color || "#000000"} 3px,
transparent 3px,
transparent 10px
)`,
backgroundColor: "transparent",
}),
...(component.lineStyle === "double" && {
...(component.line_style === "double" && {
boxShadow:
component.orientation === "horizontal"
? `0 ${(component.lineWidth || 1) * 2}px 0 0 ${component.lineColor || "#000000"}`
: `${(component.lineWidth || 1) * 2}px 0 0 0 ${component.lineColor || "#000000"}`,
? `0 ${(component.line_width || 1) * 2}px 0 0 ${component.line_color || "#000000"}`
: `${(component.line_width || 1) * 2}px 0 0 0 ${component.line_color || "#000000"}`,
}),
}}
/>
@@ -1353,18 +1353,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
display: "flex",
gap: "8px",
flexDirection:
component.labelPosition === "top" || component.labelPosition === "bottom"
component.label_position === "top" || component.label_position === "bottom"
? "column"
: "row",
...(component.labelPosition === "right" || component.labelPosition === "bottom"
...(component.label_position === "right" || component.label_position === "bottom"
? {
flexDirection:
component.labelPosition === "right" ? "row-reverse" : "column-reverse",
component.label_position === "right" ? "row-reverse" : "column-reverse",
}
: {}),
}}
>
{component.showLabel !== false && (
{component.show_label !== false && (
<div
style={{
display: "flex",
@@ -1373,23 +1373,23 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
fontSize: "12px",
fontWeight: "500",
minWidth:
component.labelPosition === "left" || component.labelPosition === "right"
component.label_position === "left" || component.label_position === "right"
? "40px"
: "auto",
}}
>
{component.labelText || "서명:"}
{component.label_text || "서명:"}
</div>
)}
<div style={{ flex: 1, position: "relative" }}>
{component.imageUrl && (
{component.image_url && (
<img
src={getFullImageUrl(component.imageUrl)}
src={getFullImageUrl(component.image_url)}
alt="서명"
style={{
width: "100%",
height: "100%",
objectFit: component.objectFit || "contain",
objectFit: component.object_fit || "contain",
}}
/>
)}
@@ -1406,7 +1406,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
height: "100%",
}}
>
{component.personName && (
{component.person_name && (
<div
style={{
display: "flex",
@@ -1415,7 +1415,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
fontWeight: "500",
}}
>
{component.personName}
{component.person_name}
</div>
)}
<div
@@ -1424,18 +1424,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
flex: 1,
}}
>
{component.imageUrl && (
{component.image_url && (
<img
src={getFullImageUrl(component.imageUrl)}
src={getFullImageUrl(component.image_url)}
alt="도장"
style={{
width: "100%",
height: "100%",
objectFit: component.objectFit || "contain",
objectFit: component.object_fit || "contain",
}}
/>
)}
{component.showLabel !== false && (
{component.show_label !== false && (
<div
style={{
position: "absolute",
@@ -1451,7 +1451,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
pointerEvents: "none",
}}
>
{component.labelText || "(인)"}
{component.label_text || "(인)"}
</div>
)}
</div>
@@ -1459,7 +1459,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
)}
{component.type === "pageNumber" && (() => {
const format = component.pageNumberFormat || "number";
const format = component.page_number_format || "number";
const pageIndex = layoutConfig.pages
.sort((a, b) => a.page_order - b.page_order)
.findIndex((p) => p.page_id === page.page_id);
@@ -1486,9 +1486,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
justifyContent: "center",
width: "100%",
height: "100%",
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
fontSize: `${component.font_size}px`,
color: component.font_color,
fontWeight: component.font_weight,
}}
>
{pageNumberText}
@@ -1498,22 +1498,22 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
{/* Card 컴포넌트 */}
{component.type === "card" && (() => {
const cardTitle = component.cardTitle || "정보 카드";
const cardItems = component.cardItems || [];
const labelWidth = component.labelWidth || 80;
const showCardTitle = component.showCardTitle !== false;
const titleFontSize = component.titleFontSize || 14;
const labelFontSize = component.labelFontSize || 13;
const valueFontSize = component.valueFontSize || 13;
const titleColor = component.titleColor || "#1e40af";
const labelColor = component.labelColor || "#374151";
const valueColor = component.valueColor || "#000000";
const borderColor = component.borderColor || "#e5e7eb";
const cardTitle = component.card_title || "정보 카드";
const cardItems = component.card_items || [];
const labelWidth = component.label_width || 80;
const showCardTitle = component.show_card_title !== false;
const titleFontSize = component.title_font_size || 14;
const labelFontSize = component.label_font_size || 13;
const valueFontSize = component.value_font_size || 13;
const titleColor = component.title_color || "#1e40af";
const labelColor = component.label_color || "#374151";
const valueColor = component.value_color || "#000000";
const borderColor = component.border_color || "#e5e7eb";
// 쿼리 바인딩된 값 가져오기
const getCardValue = (item: { label: string; value: string; fieldName?: string }) => {
if (item.fieldName && component.queryId) {
const qResult = getQueryResult(component.queryId);
if (item.fieldName && component.query_id) {
const qResult = getQueryResult(component.query_id);
if (qResult && qResult.rows && qResult.rows.length > 0) {
const row = qResult.rows[0];
return row[item.fieldName] !== undefined ? String(row[item.fieldName]) : item.value;
@@ -1585,18 +1585,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
{/* 계산 컴포넌트 */}
{component.type === "calculation" && (() => {
const calcItems = component.calcItems || [];
const resultLabel = component.resultLabel || "합계";
const calcLabelWidth = component.labelWidth || 120;
const calcLabelFontSize = component.labelFontSize || 13;
const calcValueFontSize = component.valueFontSize || 13;
const calcResultFontSize = component.resultFontSize || 16;
const calcLabelColor = component.labelColor || "#374151";
const calcValueColor = component.valueColor || "#000000";
const calcResultColor = component.resultColor || "#2563eb";
const numberFormat = component.numberFormat || "currency";
const currencySuffix = component.currencySuffix || "원";
const borderColor = component.borderColor || "#374151";
const calcItems = component.calc_items || [];
const resultLabel = component.result_label || "합계";
const calcLabelWidth = component.label_width || 120;
const calcLabelFontSize = component.label_font_size || 13;
const calcValueFontSize = component.value_font_size || 13;
const calcResultFontSize = component.result_font_size || 16;
const calcLabelColor = component.label_color || "#374151";
const calcValueColor = component.value_color || "#000000";
const calcResultColor = component.result_color || "#2563eb";
const numberFormat = component.number_format || "currency";
const currencySuffix = component.currency_suffix || "원";
const borderColor = component.border_color || "#374151";
// 숫자 포맷팅 함수
const formatNumber = (num: number): string => {
@@ -1608,8 +1608,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
// 쿼리 바인딩된 값 가져오기
const getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => {
if (item.fieldName && component.queryId) {
const qResult = getQueryResult(component.queryId);
if (item.fieldName && component.query_id) {
const qResult = getQueryResult(component.query_id);
if (qResult && qResult.rows && qResult.rows.length > 0) {
const row = qResult.rows[0];
const val = row[item.fieldName];
@@ -1715,18 +1715,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
{/* 체크박스 컴포넌트 */}
{component.type === "checkbox" && (() => {
const checkboxSize = component.checkboxSize || 18;
const checkboxColor = component.checkboxColor || "#2563eb";
const checkboxBorderColor = component.checkboxBorderColor || "#6b7280";
const checkboxLabel = component.checkboxLabel || "";
const checkboxLabelPosition = component.checkboxLabelPosition || "right";
const checkboxSize = component.checkbox_size || 18;
const checkboxColor = component.checkbox_color || "#2563eb";
const checkboxBorderColor = component.checkbox_border_color || "#6b7280";
const checkboxLabel = component.checkbox_label || "";
const checkboxLabelPosition = component.checkbox_label_position || "right";
// 체크 상태 결정
let isChecked = component.checkboxChecked === true;
if (component.checkboxFieldName && component.queryId) {
const qResult = getQueryResult(component.queryId);
let isChecked = component.checkbox_checked === true;
if (component.checkbox_field_name && component.query_id) {
const qResult = getQueryResult(component.query_id);
if (qResult && qResult.rows && qResult.rows.length > 0) {
const val = qResult.rows[0][component.checkboxFieldName];
const val = qResult.rows[0][component.checkbox_field_name];
isChecked = val === true || val === "Y" || val === "1" || val === 1 || val === "true";
}
}
@@ -35,28 +35,28 @@ export function ScreenSplitPanel({ screenId, config, initialFormData, groupedDat
const leftEmbedding = config?.leftScreenId
? {
id: 1,
parentScreenId: screenId || 0,
childScreenId: config.leftScreenId,
parent_screen_id: screenId || 0,
child_screen_id: config.leftScreenId,
position: "left" as const,
mode: "view" as const,
config: {},
companyCode: "*",
createdAt: now,
updatedAt: now,
company_code: "*",
created_at: now,
updated_at: now,
}
: null;
const rightEmbedding = config?.rightScreenId
? {
id: 2,
parentScreenId: screenId || 0,
childScreenId: config.rightScreenId,
parent_screen_id: screenId || 0,
child_screen_id: config.rightScreenId,
position: "right" as const,
mode: "view" as const,
config: {},
companyCode: "*",
createdAt: now,
updatedAt: now,
company_code: "*",
created_at: now,
updated_at: now,
}
: null;
@@ -94,12 +94,12 @@ export function ScreenSplitPanel({ screenId, config, initialFormData, groupedDat
return (
<SplitPanelProvider
splitPanelId={splitPanelId}
leftScreenId={config?.leftScreenId || null}
rightScreenId={config?.rightScreenId || null}
parentDataMapping={config?.parentDataMapping || []}
linkedFilters={config?.linkedFilters || []}
disableAutoDataTransfer={config?.disableAutoDataTransfer ?? false}
split_panel_id={splitPanelId}
left_screen_id={config?.leftScreenId || null}
right_screen_id={config?.rightScreenId || null}
parent_data_mapping={config?.parentDataMapping || []}
linked_filters={config?.linkedFilters || []}
disable_auto_data_transfer={config?.disableAutoDataTransfer ?? false}
>
<ResponsiveSplitPanel
left={
@@ -747,7 +747,7 @@ export default function CopyScreenModal({
parent_group_id: parentGroupId,
target_company_code: targetCompany,
display_order: sourceGroupData.display_order, // 원본 정렬순서 유지
});
} as any);
if (!newGroupResponse.success || !newGroupResponse.data) {
throw new Error(newGroupResponse.error || `그룹 생성 실패: ${sourceGroupData.group_name}`);
@@ -790,15 +790,16 @@ export default function CopyScreenModal({
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
for (const { screenData: screen, displayOrder, screenRole } of screensWithOrder) {
if (!screen) continue;
try {
// 미리 생성된 화면 코드 사용
const newScreenCode = screenCodes[codeIndex.current];
codeIndex.current++;
// 진행률 업데이트
setCopyProgress({
current: stats.screens + 1,
total: totalScreenCount,
setCopyProgress({
current: stats.screens + 1,
total: totalScreenCount,
message: `화면 복제 중: ${screen.screen_name}`
});
@@ -926,7 +927,7 @@ export default function CopyScreenModal({
parent_group_id: groupParentId,
target_company_code: finalCompanyCode,
display_order: groupDisplayOrder, // 사용자가 입력한 정렬 순서
});
} as any);
if (!newGroupResponse.success || !newGroupResponse.data) {
throw new Error(newGroupResponse.error || "그룹 생성 실패");
@@ -980,6 +981,7 @@ export default function CopyScreenModal({
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
for (const { screenData: screen, displayOrder, screenRole } of screensWithOrder) {
if (!screen) continue;
try {
// 미리 생성된 화면 코드 사용
const newScreenCode = screenCodes[codeIndex.current];
@@ -46,7 +46,7 @@ export function SortableColumnRow({
<GripVertical className="h-3 w-3" />
</div>
{isEntityJoin ? (
<Link2 className="h-3 w-3 shrink-0 text-primary" title="Entity 조인 컬럼" />
<Link2 className="h-3 w-3 shrink-0 text-primary" />
) : (
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
)}
@@ -275,10 +275,10 @@ export function TableGroupedComponent({
// TABLE_SELECTION_CHANGE 이벤트 발송 (선택 데이터 변경 시 다른 컴포넌트에 알림)
v2EventBus.emit(V2_EVENTS.TABLE_SELECTION_CHANGE, {
componentId: componentId || tableId,
tableName: config.selectedTable || "",
selectedRows: selectedItems,
selectedCount: selectedItems.length,
selectedRowIds: selectedItems.map((item: any) => item.id).filter(Boolean),
source: componentId || tableId,
});
console.log("[TableGroupedComponent] 선택 변경 이벤트 발송:", {
@@ -35,7 +35,8 @@ import {
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { tableTypeApi } from "@/lib/api/screen";
import { TableGroupedConfig, ColumnConfig, LinkedFilterConfig } from "./types";
import { TableGroupedConfig, LinkedFilterConfig } from "./types";
import { ColumnConfig } from "../v2-table-list/types";
import {
groupHeaderStyleOptions,
checkboxModeOptions,
@@ -396,7 +396,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
ref={editInputRef as React.RefObject<HTMLSelectElement>}
value={editingValue ?? ""}
onChange={(e) => onEditingValueChange?.(e.target.value)}
onKeyDown={onEditKeyDown}
onKeyDown={onEditKeyDown as React.KeyboardEventHandler<HTMLSelectElement>}
onBlur={handleBlurSave}
className={cn(commonInputClass, "h-8")}
onClick={(e) => e.stopPropagation()}
@@ -417,7 +417,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
return (
<InlineCellDatePicker
value={editingValue ?? ""}
onChange={(v) => onEditingValueChange?.(v)}
onChange={(v: string) => onEditingValueChange?.(v)}
onSave={() => {
handleBlurSave();
}}
@@ -2,7 +2,6 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { TableListConfig, ColumnConfig } from "./types";
import { WebType } from "@/types/common";
import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { codeCache } from "@/lib/caching/codeCache";
@@ -60,7 +59,7 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
// 각 objid의 대표 여부를 확인
for (const objid of objids) {
const info = await getFileInfoByObjid(objid);
if (info.success && info.data?.isRepresentative) {
if (info.success && info.data?.is_representative) {
representativeId = objid;
break;
}
@@ -173,9 +172,9 @@ const TableCellFile: React.FC<{ value: string }> = React.memo(({ value }) => {
if (res.success && res.data) {
return {
objid: oid,
name: res.data.realFileName || "파일",
ext: res.data.fileExt || "",
size: res.data.fileSize || 0,
name: res.data.real_file_name || "파일",
ext: res.data.file_ext || "",
size: res.data.file_size || 0,
};
}
} catch {}
@@ -544,6 +543,7 @@ export interface TableListComponentProps {
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
// 🆕 프리뷰용 회사 코드 (DynamicComponentRenderer에서 전달, 최고 관리자만 오버라이드 가능)
companyCode?: string;
renderer?: any;
}
// ========================================
@@ -715,14 +715,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// columnVisibility 변경 시 컬럼 순서, 가시성, 너비 적용
useEffect(() => {
if (columnVisibility.length > 0) {
const newOrder = columnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 제외
const newOrder = columnVisibility.map((cv) => cv.column_name).filter((name) => name !== "__checkbox__"); // 체크박스 제외
setColumnOrder(newOrder);
// 너비 적용
const newWidths: Record<string, number> = {};
columnVisibility.forEach((cv) => {
if (cv.width) {
newWidths[cv.columnName] = cv.width;
newWidths[cv.column_name] = cv.width;
}
});
if (Object.keys(newWidths).length > 0) {
@@ -757,7 +757,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// columnVisibility가 있으면 가시성 적용
if (columnVisibility.length > 0) {
cols = cols.filter((col) => {
const visibilityConfig = columnVisibility.find((cv) => cv.columnName === col.columnName);
const visibilityConfig = columnVisibility.find((cv) => cv.column_name === col.columnName);
return visibilityConfig ? visibilityConfig.visible : true;
});
}
@@ -1167,10 +1167,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return (tableConfig.columns || [])
.filter((col) => col.additionalJoinInfo)
.map((col) => ({
sourceTable: col.additionalJoinInfo!.sourceTable || tableConfig.selectedTable,
sourceColumn: col.additionalJoinInfo!.sourceColumn,
joinAlias: col.additionalJoinInfo!.joinAlias,
referenceTable: col.additionalJoinInfo!.referenceTable,
source_table: col.additionalJoinInfo!.sourceTable || tableConfig.selectedTable,
source_column: col.additionalJoinInfo!.sourceColumn,
join_alias: col.additionalJoinInfo!.joinAlias,
reference_table: col.additionalJoinInfo!.referenceTable,
}));
},
};
@@ -1354,8 +1354,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
table_name: tableConfig.selectedTable,
data_count: totalItems || data.length, // 초기 데이터 건수 포함
columns: columnsToRegister.map((col) => ({
column_name: col.columnName || col.field,
column_label: columnLabels[col.columnName] || col.displayName || col.label || col.columnName || col.field,
column_name: col.columnName,
column_label: columnLabels[col.columnName] || col.displayName || col.columnName,
input_type: columnMeta[col.columnName]?.inputType || "text",
visible: col.visible !== false,
width: columnWidths[col.columnName] || col.width || 150,
@@ -1373,7 +1373,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setFrozenColumnCount(count);
const visibleCols = columnsToRegister
.filter((col) => col.visible !== false)
.map((col) => col.columnName || col.field);
.map((col) => col.columnName);
setFrozenColumns(visibleCols.slice(0, count));
},
// 탭 관련 정보 (탭 내부의 테이블인 경우)
@@ -1477,21 +1477,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
tableConfig.selectedTable,
initialData,
parsedOrder.filter((col) => col !== "__checkbox__"),
sortColumn,
sortColumn ?? null,
sortDirection,
{
filterConditions: Object.keys(searchValues).length > 0 ? searchValues : undefined,
searchTerm: searchTerm || undefined,
visibleColumns: cols.map((col) => col.columnName),
columnLabels: labels,
currentPage: currentPage,
pageSize: localPageSize,
totalItems: totalItems,
filter_conditions: Object.keys(searchValues).length > 0 ? searchValues : undefined,
search_term: searchTerm || undefined,
visible_columns: cols.map((col) => col.columnName),
column_labels: labels,
current_page: currentPage,
page_size: localPageSize,
total_items: totalItems,
},
);
}
onSelectedRowsChange([], [], sortColumn, sortDirection, parsedOrder, initialData);
onSelectedRowsChange([], [], sortColumn || undefined, sortDirection, parsedOrder, initialData);
}
} catch (error) {
console.error("❌ 컬럼 순서 파싱 실패:", error);
@@ -1871,16 +1871,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (splitPanelContext) {
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
const linkedFiltersConfig = splitPanelContext.linked_filters || [];
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
(filter) =>
(filter: any) =>
filter.targetColumn?.startsWith(tableConfig.selectedTable + ".") ||
filter.targetColumn === tableConfig.selectedTable,
);
// 좌측 데이터 선택 여부 확인
hasSelectedLeftData =
splitPanelContext.selected_left_data && Object.keys(splitPanelContext.selected_left_data).length > 0;
!!(splitPanelContext.selected_left_data && Object.keys(splitPanelContext.selected_left_data).length > 0);
const allLinkedFilters = splitPanelContext.getLinkedFilterValues();
@@ -2065,7 +2065,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 전달
dataFilter: tableConfig.dataFilter, // 🆕 데이터 필터 전달
excludeFilter: excludeFilterParam, // 🆕 제외 필터 전달
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만)
company_code_override: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만)
});
// 실제 데이터의 item_number만 추출하여 중복 확인
@@ -2096,16 +2096,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
tableConfig.selectedTable,
response.data || [],
cols.map((col) => col.columnName),
sortBy,
sortBy ?? null,
sortOrder,
{
filterConditions: filters,
searchTerm: search,
visibleColumns: cols.map((col) => col.columnName),
columnLabels: labels,
currentPage: page,
pageSize: pageSize,
totalItems: response.total || 0,
filter_conditions: filters,
search_term: search,
visible_columns: cols.map((col) => col.columnName),
column_labels: labels,
current_page: page,
page_size: pageSize,
total_items: response.total || 0,
},
);
}
@@ -2131,7 +2131,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 🆕 우측 화면일 때만 selectedLeftData 변경에 반응 (좌측 테이블은 재조회 불필요)
splitPanelPosition,
currentSplitPosition,
splitPanelContext?.selectedLeftData,
splitPanelContext?.selected_left_data,
// 🆕 RelatedDataButtons 필터 추가
relatedButtonFilter,
isRelatedButtonTarget,
@@ -2383,8 +2383,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
const modalItems = selectedRowsData.map((row, idx) => ({
id: getRowKey(row, idx),
originalData: row,
additionalData: {},
original_data: row,
additional_data: {},
}));
useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
});
@@ -2429,8 +2429,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
const modalItems = filteredData.map((row, idx) => ({
id: getRowKey(row, idx),
originalData: row,
additionalData: {},
original_data: row,
additional_data: {},
}));
useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
@@ -469,13 +469,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// 🎯 joinTables에서 sourceColumn 찾기
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
const sourceColumn = (joinTableInfo as any)?.joinConfig?.sourceColumn || "";
console.log("🔍 조인 정보 추출:", {
tableName: joinColumn.tableName,
foundJoinTable: !!joinTableInfo,
sourceColumn,
joinConfig: joinTableInfo?.joinConfig,
joinConfig: (joinTableInfo as any)?.joinConfig,
});
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
@@ -1540,7 +1540,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
</button>
)}
<span className={cn("text-[10px] text-primary/80", !isAlreadyAdded && "ml-auto")}>
{column.inputType || column.dataType}
{(column as any).inputType || column.dataType}
</span>
</div>
);
@@ -489,7 +489,7 @@ export const ColumnsConfigPanel: React.FC<ColumnsConfigPanelProps> = ({
</button>
)}
<span className={cn("text-[10px] text-primary/80", !isAlreadyAdded && "ml-auto")}>
{column.inputType || column.dataType}
{(column as any).inputType || column.dataType}
</span>
</div>
);
@@ -18,18 +18,6 @@ ComponentRegistry.registerComponent({
web_type: "custom" as any,
default_size: { width: 1920, height: 80 }, // 픽셀 단위: 전체 너비 × 80px 높이
component: TableSearchWidget,
defaultProps: {
title: "테이블 검색",
style: {
width: "100%",
height: "80px",
padding: "0.75rem",
},
componentConfig: {
autoSelectFirstTable: true,
showTableSelector: true,
},
},
renderer: TableSearchWidgetRenderer.render as any,
config_panel: V2TableSearchWidgetConfigPanel,
version: "1.0.0",
@@ -2,12 +2,12 @@
import React from "react";
import { ComponentData } from "@/types/screen";
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
import { componentRegistry, ComponentRenderer } from "../../DynamicComponentRenderer";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// 탭 컴포넌트 렌더러
const TabsRenderer: ComponentRenderer = ({ component, children, ...props }) => {
const config = component.componentConfig || {};
const config = component.component_config || {};
const {
tabs = [
{ id: "tab1", label: "탭 1", content: "첫 번째 탭 내용" },
@@ -432,7 +432,7 @@ const TabsDesignEditor: React.FC<{
return {
...t,
components: (t.components || []).map((c) =>
c.id === comp.id ? { ...c, componentConfig: updated.componentConfig || updated.overrides || c.componentConfig } : c
c.id === comp.id ? { ...c, component_config: updated.componentConfig || updated.overrides || c.component_config } : c
),
};
});
@@ -600,131 +600,6 @@ ComponentRegistry.registerComponent({
height: 600,
},
// 에디터 모드에서의 렌더링 - 탭 선택 및 컴포넌트 드롭 지원
renderEditor: ({
component,
isSelected,
onClick,
onDragStart,
onDragEnd,
}: any) => {
const tabsConfig = (component as any).componentConfig || {};
const tabs: TabItem[] = tabsConfig.tabs || [];
// 에디터 모드에서 선택된 탭 상태 관리
const [activeTabId, setActiveTabId] = useState<string>(
tabs[0]?.id || ""
);
const activeTab = tabs.find((t) => t.id === activeTabId);
// 탭 스타일 클래스
const getTabStyle = (tab: TabItem) => {
const isActive = tab.id === activeTabId;
return cn(
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
isActive
? "bg-primary/10 border-b-2 border-primary text-primary font-semibold"
: "text-foreground/70 hover:text-foreground hover:bg-muted/50"
);
};
return (
<div
className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background"
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
{/* 탭 헤더 */}
<div className="flex items-center border-b bg-muted/50">
{tabs.length > 0 ? (
tabs.map((tab) => (
<div
key={tab.id}
className={getTabStyle(tab)}
onClick={(e) => {
e.stopPropagation();
setActiveTabId(tab.id);
}}
>
{tab.label || "탭"}
</div>
))
) : (
<div className="px-4 py-2 text-sm text-muted-foreground">
</div>
)}
</div>
{/* 탭 컨텐츠 영역 - 드롭 영역 */}
<div
className="relative flex-1 overflow-hidden"
data-tabs-container="true"
data-component-id={component.id}
data-active-tab-id={activeTabId}
>
{activeTab ? (
<div className="absolute inset-0 overflow-auto p-2">
{activeTab.components && activeTab.components.length > 0 ? (
<div className="relative h-full w-full">
{activeTab.components.map((comp: TabInlineComponent) => (
<div
key={comp.id}
className="absolute rounded border border-dashed border-input bg-white/80 p-2 shadow-sm"
style={{
left: comp.position?.x || 0,
top: comp.position?.y || 0,
width: comp.size?.width || 200,
height: comp.size?.height || 100,
}}
>
<div className="flex h-full flex-col items-center justify-center">
<span className="text-xs font-medium text-muted-foreground">
{comp.label || comp.component_type}
</span>
<span className="text-[10px] text-muted-foreground/70">
{comp.component_type}
</span>
</div>
</div>
))}
</div>
) : (
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-input bg-muted/50">
<Plus className="mb-2 h-8 w-8 text-muted-foreground/70" />
<p className="text-sm font-medium text-muted-foreground">
</p>
<p className="mt-1 text-xs text-muted-foreground/70">
</p>
</div>
)}
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-sm text-muted-foreground">
</p>
</div>
)}
</div>
{/* 선택 표시 */}
{isSelected && (
<div className="pointer-events-none absolute inset-0 rounded-lg ring-2 ring-primary ring-offset-2" />
)}
</div>
);
},
// 인터랙티브 모드에서의 렌더링
renderInteractive: ({ component }: any) => {
return null;
},
// 설정 패널
config_panel: React.lazy(() =>
import("@/components/screen/config-panels/TabsConfigPanel").then(
@@ -733,28 +608,4 @@ ComponentRegistry.registerComponent({
})
)
),
// 검증 함수
validate: (component: any) => {
const tabsConfig = (component as any).componentConfig || {};
const tabs: TabItem[] = tabsConfig.tabs || [];
const errors: string[] = [];
if (!tabs || tabs.length === 0) {
errors.push("최소 1개 이상의 탭이 필요합니다.");
}
if (tabs) {
const tabIds = tabs.map((t) => t.id);
const uniqueIds = new Set(tabIds);
if (tabIds.length !== uniqueIds.size) {
errors.push("탭 ID가 중복되었습니다.");
}
}
return {
isValid: errors.length === 0,
errors,
};
},
});
@@ -110,7 +110,7 @@ export class CardLayoutRenderer extends AutoRegisteringLayoutRenderer {
* 카드 컨테이너 스타일 계산
*/
getCardContainerStyle(): React.CSSProperties {
const cardConfig = this.props.layout.layout_config?.cardLayout || {
const cardConfig = (this.props.layout.layout_config as any)?.cardLayout || {
columns: 3,
gap: 16,
};
@@ -171,7 +171,7 @@ export class CardLayoutRenderer extends AutoRegisteringLayoutRenderer {
* 그리드 위치 계산
*/
getGridPosition(index: number): { row: number; column: number } {
const columns = this.props.layout.layout_config?.cardLayout?.columns || 3;
const columns = (this.props.layout.layout_config as any)?.cardLayout?.columns || 3;
return {
row: Math.floor(index / columns),
column: index % columns,
@@ -21,18 +21,18 @@ export const HeroSectionLayout: React.FC<HeroSectionLayoutProps> = ({
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.heroSection) {
if (!(layout.layout_config as any)?.heroSection) {
return (
<div className="error-layout flex items-center justify-center rounded border-2 border-destructive/30 bg-destructive/10 p-4">
<div className="text-center text-destructive">
<div className="font-medium">heroSection .</div>
<div className="mt-1 text-sm">layoutConfig.heroSection가 .</div>
<div className="mt-1 text-sm">layout_config.heroSection가 .</div>
</div>
</div>
);
}
const heroSectionConfig = layout.layoutConfig.heroSection;
const heroSectionConfig = (layout.layout_config as any)?.heroSection;
const containerStyle = renderer.getLayoutContainerStyle();
// heroSection 컨테이너 스타일
+1 -1
View File
@@ -69,7 +69,7 @@ async function initializeLegacyLayouts() {
variant: "default",
size: "md",
closable: false,
defaultTab: "tab1",
default_tab: "tab1",
},
},
default_zones: [
@@ -21,18 +21,18 @@ export const SplitLayout: React.FC<SplitLayoutProps> = ({
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.split) {
if (!layout.layout_config?.split) {
return (
<div className="error-layout flex items-center justify-center rounded border-2 border-destructive/30 bg-destructive/10 p-4">
<div className="text-center text-destructive">
<div className="font-medium">split .</div>
<div className="mt-1 text-sm">layoutConfig.split가 .</div>
<div className="mt-1 text-sm">layout_config.split가 .</div>
</div>
</div>
);
}
const splitConfig = layout.layoutConfig.split;
const splitConfig = layout.layout_config!.split!;
const containerStyle = renderer.getLayoutContainerStyle();
// split 컨테이너 스타일
@@ -513,7 +513,7 @@ export function PopCardListComponent({
const cartListMode = config!.cartListMode!;
// 원본 화면 미선택 시 데이터 조회하지 않음
if (!cartListMode.sourceScreenId) {
if (!cartListMode.source_screen_id) {
setLoading(false);
setRows([]);
return;
@@ -525,7 +525,7 @@ export function PopCardListComponent({
try {
// 원본 화면 레이아웃에서 설정 전체 상속 (cardTemplate, inputField, packageConfig, cardSize 등)
try {
const layoutJson = await screenApi.getLayoutPop(cartListMode.sourceScreenId!);
const layoutJson = await screenApi.getLayoutPop(cartListMode.source_screen_id!);
const componentsMap = layoutJson?.components || {};
const componentList = Object.values(componentsMap) as any[];
const matched = cartListMode.sourceComponentId
@@ -541,8 +541,8 @@ export function PopCardListComponent({
const cartFilters: Record<string, unknown> = {
status: cartListMode.statusFilter || "in_cart",
};
if (cartListMode.sourceScreenId) {
cartFilters.screen_id = String(cartListMode.sourceScreenId);
if (cartListMode.source_screen_id) {
cartFilters.screen_id = String(cartListMode.source_screen_id);
}
const result = await dataApi.getTableData("cart_items", {
size: 500,
@@ -758,7 +758,7 @@ export function PopCardListComponent({
ref={containerRef}
className={`flex h-full w-full flex-col ${className || ""}`}
>
{isCartListMode && !config?.cartListMode?.sourceScreenId ? (
{isCartListMode && !config?.cartListMode?.source_screen_id ? (
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
<p className="text-sm text-muted-foreground">
.
@@ -163,10 +163,10 @@ export function ChartItemComponent({
cy="50%"
outerRadius={containerWidth > 400 ? "70%" : "80%"}
label={
containerWidth > 250
(containerWidth > 250
? ({ name, value, percent }: { name: string; value: number; percent: number }) =>
`${name} ${abbreviateNumber(value)} (${(percent * 100).toFixed(0)}%)`
: false
: undefined) as any
}
labelLine={containerWidth > 250}
>