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