행 이동 화면 할당한 상황에서도 가능하게, 코드병합 버튼액션에 추가

This commit is contained in:
leeheejin 2025-11-04 18:31:26 +09:00
parent 66b735e864
commit 82ff18e388
19 changed files with 5655 additions and 245 deletions

View File

@ -64,8 +64,8 @@ import flowExternalDbConnectionRoutes from "./routes/flowExternalDbConnectionRou
import workHistoryRoutes from "./routes/workHistoryRoutes"; // 작업 이력 관리
import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // 테이블 변경 이력 조회
import roleRoutes from "./routes/roleRoutes"; // 권한 그룹 관리
import numberingRuleController from "./controllers/numberingRuleController"; // 채번 규칙 관리
import departmentRoutes from "./routes/departmentRoutes"; // 부서 관리
import codeMergeRoutes from "./routes/codeMergeRoutes"; // 코드 병합
import { BatchSchedulerService } from "./services/batchSchedulerService";
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
@ -224,8 +224,8 @@ app.use("/api/flow", flowRoutes); // 플로우 관리 (마지막에 등록하여
app.use("/api/work-history", workHistoryRoutes); // 작업 이력 관리
app.use("/api/table-history", tableHistoryRoutes); // 테이블 변경 이력 조회
app.use("/api/roles", roleRoutes); // 권한 그룹 관리
app.use("/api/numbering-rules", numberingRuleController); // 채번 규칙 관리
app.use("/api/departments", departmentRoutes); // 부서 관리
app.use("/api/code-merge", codeMergeRoutes); // 코드 병합
// app.use("/api/collections", collectionRoutes); // 임시 주석
// app.use("/api/batch", batchRoutes); // 임시 주석
// app.use('/api/users', userRoutes);

View File

@ -3084,3 +3084,84 @@ export const resetUserPassword = async (
});
}
};
/**
* ( )
*/
export async function getTableSchema(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const companyCode = req.user?.companyCode;
if (!tableName) {
res.status(400).json({
success: false,
message: "테이블명이 필요합니다.",
});
return;
}
logger.info("테이블 스키마 조회", { tableName, companyCode });
// information_schema에서 컬럼 정보 가져오기
const schemaQuery = `
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position
`;
const columns = await query<any>(schemaQuery, [tableName]);
if (columns.length === 0) {
res.status(404).json({
success: false,
message: `테이블 '${tableName}'을 찾을 수 없습니다.`,
});
return;
}
// 컬럼 정보를 간단한 형태로 변환
const columnList = columns.map((col: any) => ({
name: col.column_name,
type: col.data_type,
nullable: col.is_nullable === "YES",
default: col.column_default,
maxLength: col.character_maximum_length,
precision: col.numeric_precision,
scale: col.numeric_scale,
}));
logger.info(`테이블 스키마 조회 성공: ${columnList.length}개 컬럼`);
res.json({
success: true,
message: "테이블 스키마 조회 성공",
data: {
tableName,
columns: columnList,
},
});
} catch (error) {
logger.error("테이블 스키마 조회 중 오류 발생:", error);
res.status(500).json({
success: false,
message: "테이블 스키마 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_SCHEMA_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
});
}
}

View File

@ -0,0 +1,282 @@
import { Request, Response } from "express";
import pool from "../database/db";
import { logger } from "../utils/logger";
interface AuthenticatedRequest extends Request {
user?: {
userId: string;
userName: string;
companyCode: string;
};
}
/**
* -
* () ,
*/
export async function mergeCodeAllTables(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const { columnName, oldValue, newValue } = req.body;
const companyCode = req.user?.companyCode;
try {
// 입력값 검증
if (!columnName || !oldValue || !newValue) {
res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (columnName, oldValue, newValue)",
});
return;
}
if (!companyCode) {
res.status(401).json({
success: false,
message: "인증 정보가 없습니다.",
});
return;
}
// 같은 값으로 병합 시도 방지
if (oldValue === newValue) {
res.status(400).json({
success: false,
message: "기존 값과 새 값이 동일합니다.",
});
return;
}
logger.info("코드 병합 시작", {
columnName,
oldValue,
newValue,
companyCode,
userId: req.user?.userId,
});
// PostgreSQL 함수 호출
const result = await pool.query(
"SELECT * FROM merge_code_all_tables($1, $2, $3, $4)",
[columnName, oldValue, newValue, companyCode]
);
// 결과 처리 (pool.query 반환 타입 처리)
const affectedTables = Array.isArray(result) ? result : (result.rows || []);
const totalRows = affectedTables.reduce(
(sum, row) => sum + parseInt(row.rows_updated || 0),
0
);
logger.info("코드 병합 완료", {
columnName,
oldValue,
newValue,
affectedTablesCount: affectedTables.length,
totalRowsUpdated: totalRows,
});
res.json({
success: true,
message: `코드 병합 완료: ${oldValue}${newValue}`,
data: {
columnName,
oldValue,
newValue,
affectedTables: affectedTables.map((row) => ({
tableName: row.table_name,
rowsUpdated: parseInt(row.rows_updated),
})),
totalRowsUpdated: totalRows,
},
});
} catch (error: any) {
logger.error("코드 병합 실패:", {
error: error.message,
stack: error.stack,
columnName,
oldValue,
newValue,
});
res.status(500).json({
success: false,
message: "코드 병합 중 오류가 발생했습니다.",
error: {
code: "CODE_MERGE_ERROR",
details: error.message,
},
});
}
}
/**
*
*/
export async function getTablesWithColumn(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const { columnName } = req.params;
try {
if (!columnName) {
res.status(400).json({
success: false,
message: "컬럼명이 필요합니다.",
});
return;
}
logger.info("컬럼을 가진 테이블 목록 조회", { columnName });
const query = `
SELECT DISTINCT t.table_name
FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name
WHERE c.column_name = $1
AND t.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
AND EXISTS (
SELECT 1 FROM information_schema.columns c2
WHERE c2.table_name = t.table_name
AND c2.column_name = 'company_code'
)
ORDER BY t.table_name
`;
const result = await pool.query(query, [columnName]);
logger.info(`컬럼을 가진 테이블 조회 완료: ${result.rows.length}`);
res.json({
success: true,
message: "테이블 목록 조회 성공",
data: {
columnName,
tables: result.rows.map((row) => row.table_name),
count: result.rows.length,
},
});
} catch (error: any) {
logger.error("테이블 목록 조회 실패:", error);
res.status(500).json({
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_LIST_ERROR",
details: error.message,
},
});
}
}
/**
* ( )
*/
export async function previewCodeMerge(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const { columnName, oldValue } = req.body;
const companyCode = req.user?.companyCode;
try {
if (!columnName || !oldValue) {
res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (columnName, oldValue)",
});
return;
}
if (!companyCode) {
res.status(401).json({
success: false,
message: "인증 정보가 없습니다.",
});
return;
}
logger.info("코드 병합 미리보기", { columnName, oldValue, companyCode });
// 해당 컬럼을 가진 테이블 찾기
const tablesQuery = `
SELECT DISTINCT t.table_name
FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name
WHERE c.column_name = $1
AND t.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
AND EXISTS (
SELECT 1 FROM information_schema.columns c2
WHERE c2.table_name = t.table_name
AND c2.column_name = 'company_code'
)
`;
const tablesResult = await pool.query(tablesQuery, [columnName]);
// 각 테이블에서 영향받을 행 수 계산
const preview = [];
const tableRows = Array.isArray(tablesResult) ? tablesResult : (tablesResult.rows || []);
for (const row of tableRows) {
const tableName = row.table_name;
// 동적 SQL 생성 (테이블명과 컬럼명은 파라미터 바인딩 불가)
// SQL 인젝션 방지: 테이블명과 컬럼명은 information_schema에서 검증된 값
const countQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${columnName}" = $1 AND company_code = $2`;
try {
const countResult = await pool.query(countQuery, [oldValue, companyCode]);
const count = parseInt(countResult.rows[0].count);
if (count > 0) {
preview.push({
tableName,
affectedRows: count,
});
}
} catch (error: any) {
logger.warn(`테이블 ${tableName} 조회 실패:`, error.message);
// 테이블 접근 실패 시 건너뛰기
continue;
}
}
const totalRows = preview.reduce((sum, item) => sum + item.affectedRows, 0);
logger.info("코드 병합 미리보기 완료", {
tablesCount: preview.length,
totalRows,
});
res.json({
success: true,
message: "코드 병합 미리보기 완료",
data: {
columnName,
oldValue,
preview,
totalAffectedRows: totalRows,
},
});
} catch (error: any) {
logger.error("코드 병합 미리보기 실패:", error);
res.status(500).json({
success: false,
message: "코드 병합 미리보기 중 오류가 발생했습니다.",
error: {
code: "PREVIEW_ERROR",
details: error.message,
},
});
}
}

View File

@ -24,6 +24,7 @@ import {
deleteCompany, // 회사 삭제
getUserLocale,
setUserLocale,
getTableSchema, // 테이블 스키마 조회
} from "../controllers/adminController";
import { authenticateToken } from "../middleware/authMiddleware";
@ -67,4 +68,7 @@ router.delete("/companies/:companyCode", deleteCompany); // 회사 삭제
router.get("/user-locale", getUserLocale);
router.post("/user-locale", setUserLocale);
// 테이블 스키마 API (엑셀 업로드 컬럼 매핑용)
router.get("/tables/:tableName/schema", getTableSchema);
export default router;

View File

@ -0,0 +1,35 @@
import express from "express";
import {
mergeCodeAllTables,
getTablesWithColumn,
previewCodeMerge,
} from "../controllers/codeMergeController";
import { authenticateToken } from "../middleware/authMiddleware";
const router = express.Router();
// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
/**
* POST /api/code-merge/merge-all-tables
* ( )
* Body: { columnName, oldValue, newValue }
*/
router.post("/merge-all-tables", mergeCodeAllTables);
/**
* GET /api/code-merge/tables-with-column/:columnName
*
*/
router.get("/tables-with-column/:columnName", getTablesWithColumn);
/**
* POST /api/code-merge/preview
* ( )
* Body: { columnName, oldValue }
*/
router.post("/preview", previewCodeMerge);
export default router;

3915
docs/품목정보.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,11 @@ export default function ScreenViewPage() {
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
// 테이블 정렬 정보 (엑셀 다운로드용)
const [tableSortBy, setTableSortBy] = useState<string | undefined>();
const [tableSortOrder, setTableSortOrder] = useState<"asc" | "desc">("asc");
const [tableColumnOrder, setTableColumnOrder] = useState<string[] | undefined>();
// 플로우에서 선택된 데이터 (버튼 액션에 전달)
const [flowSelectedData, setFlowSelectedData] = useState<any[]>([]);
const [flowSelectedStepId, setFlowSelectedStepId] = useState<number | null>(null);
@ -425,9 +430,16 @@ export default function ScreenViewPage() {
userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
sortBy={tableSortBy}
sortOrder={tableSortOrder}
columnOrder={tableColumnOrder}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => {
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder });
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
setTableColumnOrder(columnOrder);
}}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
@ -479,9 +491,16 @@ export default function ScreenViewPage() {
userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
sortBy={tableSortBy}
sortOrder={tableSortOrder}
columnOrder={tableColumnOrder}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => {
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder });
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
setTableColumnOrder(columnOrder);
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
@ -613,8 +632,14 @@ export default function ScreenViewPage() {
userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
sortBy={tableSortBy}
sortOrder={tableSortOrder}
columnOrder={tableColumnOrder}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => {
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
setTableColumnOrder(columnOrder);
}}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}

File diff suppressed because it is too large Load Diff

View File

@ -63,6 +63,10 @@ interface RealtimePreviewProps {
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
// 플로우 선택 데이터 전달용
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
// 테이블 정렬 정보 전달용
sortBy?: string;
sortOrder?: "asc" | "desc";
[key: string]: any; // 추가 props 허용
}
// 영역 레이아웃에 따른 아이콘 반환
@ -225,6 +229,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onGroupToggle,
children,
onFlowSelectedDataChange,
sortBy,
sortOrder,
...restProps
}) => {
const { user } = useAuth();
const { type, id, position, size, style = {} } = component;
@ -545,7 +552,13 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
{/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */}
{type === "widget" && !isFileComponent(component) && (
<div className="pointer-events-none h-full w-full">
<WidgetRenderer component={component} isDesignMode={isDesignMode} />
<WidgetRenderer
component={component}
isDesignMode={isDesignMode}
sortBy={sortBy}
sortOrder={sortOrder}
{...restProps}
/>
</div>
)}

View File

@ -54,6 +54,11 @@ interface RealtimePreviewProps {
// 폼 데이터 관련 props
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
// 테이블 정렬 정보
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@ -109,6 +114,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onFlowSelectedDataChange,
refreshKey,
onRefresh,
sortBy,
sortOrder,
columnOrder,
flowRefreshKey,
onFlowRefresh,
formData,
@ -395,6 +403,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onFlowRefresh={onFlowRefresh}
formData={formData}
onFormDataChange={onFormDataChange}
sortBy={sortBy}
sortOrder={sortOrder}
columnOrder={columnOrder}
/>
</div>

View File

@ -270,6 +270,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
<SelectItem value="excel_download"> </SelectItem>
<SelectItem value="excel_upload"> </SelectItem>
<SelectItem value="barcode_scan"> </SelectItem>
<SelectItem value="code_merge"> </SelectItem>
</SelectContent>
</Select>
</div>
@ -838,6 +839,53 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div>
)}
{/* 코드 병합 액션 설정 */}
{(component.componentConfig?.action?.type || "save") === "code_merge" && (
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
<h4 className="text-sm font-medium text-foreground">🔀 </h4>
<div>
<Label htmlFor="merge-column-name">
<span className="text-destructive">*</span>
</Label>
<Input
id="merge-column-name"
placeholder="예: item_code, product_id"
value={config.action?.mergeColumnName || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.mergeColumnName", e.target.value)}
className="h-8 text-xs"
/>
<p className="mt-1 text-xs text-muted-foreground">
(: item_code). .
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="merge-show-preview"> </Label>
<p className="text-xs text-muted-foreground"> </p>
</div>
<Switch
id="merge-show-preview"
checked={config.action?.mergeShowPreview !== false}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.mergeShowPreview", checked)}
/>
</div>
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
<p className="text-xs text-blue-900 dark:text-blue-100">
<strong> :</strong>
<br />
1.
<br />
2.
<br />
3. ,
</p>
</div>
</div>
)}
{/* 제어 기능 섹션 */}
<div className="mt-8 border-t border-border pt-6">
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />

View File

@ -0,0 +1,45 @@
import { apiClient } from "./client";
export interface TableColumn {
name: string;
type: string;
nullable: boolean;
default: string | null;
maxLength: number | null;
precision: number | null;
scale: number | null;
}
export interface TableSchemaResponse {
success: boolean;
message: string;
data: {
tableName: string;
columns: TableColumn[];
};
}
/**
* ( )
*/
export async function getTableSchema(
tableName: string
): Promise<TableSchemaResponse> {
try {
const response = await apiClient.get<TableSchemaResponse>(
`/admin/tables/${tableName}/schema`
);
return response.data;
} catch (error: any) {
console.error("테이블 스키마 조회 실패:", error);
return {
success: false,
message: error.response?.data?.message || "테이블 스키마 조회 실패",
data: {
tableName,
columns: [],
},
};
}
}

View File

@ -142,7 +142,7 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
const preloadCommonCodesOnMount = useCallback(async (): Promise<void> => {
if (!preloadCommonCodes) return;
console.log("🚀 공통 코드 프리로딩 시작");
// console.log("🚀 공통 코드 프리로딩 시작");
// 현재 테이블의 코드 카테고리와 공통 카테고리 합치기
const allCategories = [...new Set([...codeCategories, ...commonCodeCategories])];

View File

@ -29,7 +29,10 @@ export interface ComponentRenderer {
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[]) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
@ -101,7 +104,11 @@ export interface DynamicComponentRendererProps {
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[]) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
@ -191,6 +198,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
selectedRows,
selectedRowsData,
onSelectedRowsChange,
sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향
flowSelectedData,
flowSelectedStepId,
onFlowSelectedDataChange,
@ -278,6 +287,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
selectedRows,
selectedRowsData,
onSelectedRowsChange,
// 테이블 정렬 정보 전달
sortBy,
sortOrder,
// 플로우 선택된 데이터 정보 전달
flowSelectedData,
flowSelectedStepId,

View File

@ -49,6 +49,7 @@ const WidgetRenderer: ComponentRenderer = ({ component, ...props }) => {
value: undefined, // 미리보기이므로 값은 없음
readonly: readonly,
isDesignMode: true, // 디자인 모드임을 명시
...props, // 모든 추가 props 전달 (sortBy, sortOrder 등)
}}
config={widget.webTypeConfig}
/>

View File

@ -42,6 +42,11 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
@ -74,6 +79,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
onRefresh,
onClose,
onFlowRefresh,
sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향
columnOrder, // 🆕 컬럼 순서
selectedRows,
selectedRowsData,
flowSelectedData,
@ -405,6 +413,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 테이블 선택된 행 정보 추가
selectedRows,
selectedRowsData,
// 테이블 정렬 정보 추가
sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향
columnOrder, // 🆕 컬럼 순서
// 플로우 선택된 데이터 정보 추가
flowSelectedData,
flowSelectedStepId,

View File

@ -8,41 +8,53 @@ import { cn } from "@/lib/utils";
import { ColumnConfig } from "./types";
interface SingleTableWithStickyProps {
visibleColumns: ColumnConfig[];
visibleColumns?: ColumnConfig[];
columns?: ColumnConfig[];
data: Record<string, any>[];
columnLabels: Record<string, string>;
sortColumn: string | null;
sortDirection: "asc" | "desc";
tableConfig: any;
isDesignMode: boolean;
isAllSelected: boolean;
handleSort: (columnName: string) => void;
handleSelectAll: (checked: boolean) => void;
handleRowClick: (row: any) => void;
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
tableConfig?: any;
isDesignMode?: boolean;
isAllSelected?: boolean;
handleSort?: (columnName: string) => void;
onSort?: (columnName: string) => void;
handleSelectAll?: (checked: boolean) => void;
handleRowClick?: (row: any) => void;
renderCheckboxCell?: (row: any, index: number) => React.ReactNode;
renderCheckboxHeader?: () => React.ReactNode;
formatCellValue: (value: any, format?: string, columnName?: string, rowData?: Record<string, any>) => string;
getColumnWidth: (column: ColumnConfig) => number;
containerWidth?: string; // 컨테이너 너비 설정
loading?: boolean;
error?: string | null;
}
export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
visibleColumns,
columns,
data,
columnLabels,
sortColumn,
sortDirection,
tableConfig,
isDesignMode,
isAllSelected,
isDesignMode = false,
isAllSelected = false,
handleSort,
onSort,
handleSelectAll,
handleRowClick,
renderCheckboxCell,
renderCheckboxHeader,
formatCellValue,
getColumnWidth,
containerWidth,
loading = false,
error = null,
}) => {
const checkboxConfig = tableConfig.checkbox || {};
const checkboxConfig = tableConfig?.checkbox || {};
const actualColumns = visibleColumns || columns || [];
const sortHandler = onSort || handleSort || (() => {});
return (
<div
@ -71,15 +83,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
}
>
<TableRow className="border-b">
{visibleColumns.map((column, colIndex) => {
{actualColumns.map((column, colIndex) => {
// 왼쪽 고정 컬럼들의 누적 너비 계산
const leftFixedWidth = visibleColumns
const leftFixedWidth = actualColumns
.slice(0, colIndex)
.filter((col) => col.fixed === "left")
.reduce((sum, col) => sum + getColumnWidth(col), 0);
// 오른쪽 고정 컬럼들의 누적 너비 계산
const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right");
const rightFixedColumns = actualColumns.filter((col) => col.fixed === "right");
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
const rightFixedWidth =
rightFixedIndex >= 0
@ -115,7 +127,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
...(column.fixed === "left" && { left: leftFixedWidth }),
...(column.fixed === "right" && { right: rightFixedWidth }),
}}
onClick={() => column.sortable && handleSort(column.columnName)}
onClick={() => column.sortable && sortHandler(column.columnName)}
>
<div className="flex items-center gap-2">
{column.columnName === "__checkbox__" ? (

View File

@ -138,7 +138,8 @@ export interface TableListComponentProps {
onRefresh?: () => void;
onClose?: () => void;
screenId?: string;
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
userId?: string; // 사용자 ID (컬럼 순서 저장용)
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[]) => void;
onConfigChange?: (config: any) => void;
refreshKey?: number;
}
@ -163,6 +164,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onConfigChange,
refreshKey,
tableName,
userId,
}) => {
// ========================================
// 설정 및 스타일
@ -178,18 +180,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
let finalSelectedTable =
componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName;
console.log("🔍 TableListComponent 초기화:", {
componentConfigSelectedTable: componentConfig?.selectedTable,
componentConfigSelectedTableType: typeof componentConfig?.selectedTable,
componentConfigSelectedTable2: component.config?.selectedTable,
componentConfigSelectedTable2Type: typeof component.config?.selectedTable,
configSelectedTable: config?.selectedTable,
configSelectedTableType: typeof config?.selectedTable,
screenTableName: tableName,
screenTableNameType: typeof tableName,
finalSelectedTable,
finalSelectedTableType: typeof finalSelectedTable,
});
// 디버그 로그 제거 (성능 최적화)
// 객체인 경우 tableName 속성 추출 시도
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
@ -200,12 +191,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
tableConfig.selectedTable = finalSelectedTable;
console.log(
"✅ 최종 tableConfig.selectedTable:",
tableConfig.selectedTable,
"타입:",
typeof tableConfig.selectedTable,
);
// 디버그 로그 제거 (성능 최적화)
const buttonColor = component.style?.labelColor || "#212121";
const buttonTextColor = component.config?.buttonTextColor || "#ffffff";
@ -262,6 +248,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [isDragging, setIsDragging] = useState(false);
const [draggedRowIndex, setDraggedRowIndex] = useState<number | null>(null);
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
const [draggedColumnIndex, setDraggedColumnIndex] = useState<number | null>(null);
const [dragOverColumnIndex, setDragOverColumnIndex] = useState<number | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [columnOrder, setColumnOrder] = useState<string[]>([]);
const columnRefs = useRef<Record<string, HTMLTableCellElement | null>>({});
const [isAllSelected, setIsAllSelected] = useState(false);
const hasInitializedWidths = useRef(false);
@ -390,10 +380,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return;
}
// 테이블명 확인 로그
console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable);
console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable);
console.log("🔍 전체 tableConfig:", tableConfig);
// 테이블명 확인 로그 (개발 시에만)
// console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable);
// console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable);
// console.log("🔍 전체 tableConfig:", tableConfig);
setLoading(true);
setError(null);
@ -488,11 +478,43 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
};
const handleSort = (column: string) => {
console.log("🔄 정렬 클릭:", { column, currentSortColumn: sortColumn, currentSortDirection: sortDirection });
let newSortColumn = column;
let newSortDirection: "asc" | "desc" = "asc";
if (sortColumn === column) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
newSortDirection = sortDirection === "asc" ? "desc" : "asc";
setSortDirection(newSortDirection);
} else {
setSortColumn(column);
setSortDirection("asc");
newSortColumn = column;
newSortDirection = "asc";
}
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
// 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
if (onSelectedRowsChange) {
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
console.log("✅ 정렬 정보 전달:", {
selectedRowsCount: selectedRows.size,
selectedRowsDataCount: selectedRowsData.length,
sortBy: newSortColumn,
sortOrder: newSortDirection,
columnOrder: columnOrder.length > 0 ? columnOrder : undefined
});
onSelectedRowsChange(
Array.from(selectedRows),
selectedRowsData,
newSortColumn,
newSortDirection,
columnOrder.length > 0 ? columnOrder : undefined
);
} else {
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
}
};
@ -530,7 +552,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const selectedRowsData = data.filter((row, index) => newSelectedRows.has(getRowKey(row, index)));
if (onSelectedRowsChange) {
onSelectedRowsChange(Array.from(newSelectedRows), selectedRowsData);
onSelectedRowsChange(Array.from(newSelectedRows), selectedRowsData, sortColumn || undefined, sortDirection);
}
if (onFormDataChange) {
onFormDataChange({
@ -551,7 +573,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setIsAllSelected(true);
if (onSelectedRowsChange) {
onSelectedRowsChange(Array.from(newSelectedRows), data);
onSelectedRowsChange(Array.from(newSelectedRows), data, sortColumn || undefined, sortDirection);
}
if (onFormDataChange) {
onFormDataChange({
@ -564,7 +586,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setIsAllSelected(false);
if (onSelectedRowsChange) {
onSelectedRowsChange([], []);
onSelectedRowsChange([], [], sortColumn || undefined, sortDirection);
}
if (onFormDataChange) {
onFormDataChange({ selectedRows: [], selectedRowsData: [] });
@ -588,6 +610,58 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setDraggedRowIndex(null);
};
// 컬럼 드래그앤드롭 핸들러
const handleColumnDragStart = (e: React.DragEvent, columnIndex: number) => {
console.log("🔄 컬럼 드래그 시작:", columnIndex);
setDraggedColumnIndex(columnIndex);
e.dataTransfer.effectAllowed = "move";
};
const handleColumnDragOver = (e: React.DragEvent, columnIndex: number) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
if (draggedColumnIndex !== null && draggedColumnIndex !== columnIndex) {
setDragOverColumnIndex(columnIndex);
}
};
const handleColumnDrop = (e: React.DragEvent, dropColumnIndex: number) => {
e.preventDefault();
console.log("📥 컬럼 드롭:", { from: draggedColumnIndex, to: dropColumnIndex });
if (draggedColumnIndex === null || draggedColumnIndex === dropColumnIndex) {
setDraggedColumnIndex(null);
setDragOverColumnIndex(null);
return;
}
// 컬럼 순서 변경
const newColumns = [...visibleColumns];
const [draggedColumn] = newColumns.splice(draggedColumnIndex, 1);
newColumns.splice(dropColumnIndex, 0, draggedColumn);
console.log("✅ 컬럼 순서 변경 완료:", newColumns.map(c => c.columnName));
// 로컬 스토리지에 저장 (사용자별 설정)
const userKey = userId || 'guest';
const storageKey = `table_column_order_${tableConfig.selectedTable}_${userKey}`;
const newColumnOrder = newColumns.map(c => c.columnName);
localStorage.setItem(storageKey, JSON.stringify(newColumnOrder));
console.log("💾 컬럼 순서 저장:", { storageKey, columnOrder: newColumnOrder });
// 상태 직접 업데이트 - React가 즉시 리렌더링하도록
setColumnOrder(newColumnOrder);
console.log("🔄 columnOrder 상태 업데이트:", newColumnOrder);
setDraggedColumnIndex(null);
setDragOverColumnIndex(null);
};
const handleColumnDragEnd = () => {
setDraggedColumnIndex(null);
setDragOverColumnIndex(null);
};
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.();
@ -619,8 +693,26 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
}
// columnOrder 상태가 있으면 그 순서대로 정렬
if (columnOrder.length > 0) {
const orderedCols = columnOrder
.map(colName => cols.find(c => c.columnName === colName))
.filter(Boolean) as ColumnConfig[];
// columnOrder에 없는 새로운 컬럼들 추가
const remainingCols = cols.filter(c => !columnOrder.includes(c.columnName));
console.log("🔄 columnOrder 기반 정렬:", {
columnOrder,
orderedColsCount: orderedCols.length,
remainingColsCount: remainingCols.length
});
return [...orderedCols, ...remainingCols];
}
return cols.sort((a, b) => (a.order || 0) - (b.order || 0));
}, [tableConfig.columns, tableConfig.checkbox]);
}, [tableConfig.columns, tableConfig.checkbox, columnOrder]);
const getColumnWidth = (column: ColumnConfig) => {
if (column.columnName === "__checkbox__") return 50;
@ -1178,6 +1270,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
tableConfig={tableConfig}
isDesignMode={isDesignMode}
isAllSelected={isAllSelected}
handleSelectAll={handleSelectAll}
handleRowClick={handleRowClick}
columnLabels={columnLabels}
renderCheckboxHeader={renderCheckboxHeader}
renderCheckboxCell={renderCheckboxCell}
@ -1289,10 +1386,30 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<th
key={column.columnName}
ref={(el) => (columnRefs.current[column.columnName] = el)}
draggable={!isDesignMode && column.columnName !== "__checkbox__"}
onDragStart={(e) => {
if (column.columnName !== "__checkbox__") {
handleColumnDragStart(e, columnIndex);
}
}}
onDragOver={(e) => {
if (column.columnName !== "__checkbox__") {
handleColumnDragOver(e, columnIndex);
}
}}
onDrop={(e) => {
if (column.columnName !== "__checkbox__") {
handleColumnDrop(e, columnIndex);
}
}}
onDragEnd={handleColumnDragEnd}
className={cn(
"relative h-10 text-xs font-bold text-foreground/90 overflow-hidden text-ellipsis whitespace-nowrap select-none sm:h-12 sm:text-sm",
column.columnName === "__checkbox__" ? "px-0 py-2" : "px-2 py-2 sm:px-6 sm:py-3",
column.sortable && "cursor-pointer hover:bg-muted/70 transition-colors"
(column.sortable !== false && column.columnName !== "__checkbox__") && "cursor-pointer hover:bg-muted/70 transition-colors",
!isDesignMode && column.columnName !== "__checkbox__" && "cursor-move",
draggedColumnIndex === columnIndex && "opacity-50",
dragOverColumnIndex === columnIndex && "bg-primary/20"
)}
style={{
textAlign: column.columnName === "__checkbox__" ? "center" : "center",
@ -1303,7 +1420,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}}
onClick={() => {
if (isResizing.current) return;
if (column.sortable) handleSort(column.columnName);
if (column.sortable !== false && column.columnName !== "__checkbox__") {
handleSort(column.columnName);
}
}}
>
{column.columnName === "__checkbox__" ? (
@ -1311,7 +1430,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
) : (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span>{columnLabels[column.columnName] || column.displayName}</span>
{column.sortable && sortColumn === column.columnName && (
{column.sortable !== false && sortColumn === column.columnName && (
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
)}
</div>

View File

@ -20,7 +20,8 @@ export type ButtonActionType =
| "view_table_history" // 테이블 이력 보기
| "excel_download" // 엑셀 다운로드
| "excel_upload" // 엑셀 업로드
| "barcode_scan"; // 바코드 스캔
| "barcode_scan" // 바코드 스캔
| "code_merge"; // 코드 병합
/**
*
@ -73,6 +74,10 @@ export interface ButtonActionConfig {
barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
barcodeFormat?: "all" | "1d" | "2d"; // 바코드 포맷 (기본: "all")
barcodeAutoSubmit?: boolean; // 스캔 후 자동 제출 여부
// 코드 병합 관련
mergeColumnName?: string; // 병합할 컬럼명 (예: "item_code")
mergeShowPreview?: boolean; // 병합 전 미리보기 표시 여부 (기본: true)
}
/**
@ -101,8 +106,11 @@ export interface ButtonActionContext {
// 제어 실행을 위한 추가 정보
buttonId?: string;
userId?: string;
companyCode?: string;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string; // 정렬 컬럼명
sortOrder?: "asc" | "desc"; // 정렬 방향
columnOrder?: string[]; // 컬럼 순서 (사용자가 드래그앤드롭으로 변경한 순서)
}
/**
@ -147,6 +155,9 @@ export class ButtonActionExecutor {
case "barcode_scan":
return await this.handleBarcodeScan(config, context);
case "code_merge":
return await this.handleCodeMerge(config, context);
default:
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
return false;
@ -1688,17 +1699,57 @@ export class ButtonActionExecutor {
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
dataToExport = context.selectedRowsData;
console.log("✅ 선택된 행 데이터 사용:", dataToExport.length);
// 선택된 행도 정렬 적용
if (context.sortBy) {
console.log("🔄 선택된 행 데이터 정렬 적용:", {
sortBy: context.sortBy,
sortOrder: context.sortOrder,
});
dataToExport = [...dataToExport].sort((a, b) => {
const aVal = a[context.sortBy!];
const bVal = b[context.sortBy!];
// null/undefined 처리
if (aVal == null && bVal == null) return 0;
if (aVal == null) return 1;
if (bVal == null) return -1;
// 숫자 비교
const aNum = Number(aVal);
const bNum = Number(bVal);
if (!isNaN(aNum) && !isNaN(bNum)) {
return context.sortOrder === "desc" ? bNum - aNum : aNum - bNum;
}
// 문자열 비교
const aStr = String(aVal);
const bStr = String(bVal);
const comparison = aStr.localeCompare(bStr);
return context.sortOrder === "desc" ? -comparison : comparison;
});
console.log("✅ 정렬 완료:", {
firstRow: dataToExport[0],
lastRow: dataToExport[dataToExport.length - 1],
});
}
}
// 2순위: 테이블 전체 데이터 (API 호출)
else if (context.tableName) {
console.log("🔄 테이블 전체 데이터 조회 중...", context.tableName);
console.log("📊 정렬 정보:", {
sortBy: context.sortBy,
sortOrder: context.sortOrder,
});
try {
const { dynamicFormApi } = await import("@/lib/api/dynamicForm");
const response = await dynamicFormApi.getTableData(context.tableName, {
page: 1,
pageSize: 10000, // 최대 10,000개 행
sortBy: "id", // 기본 정렬: id 컬럼
sortOrder: "asc", // 오름차순
sortBy: context.sortBy || "id", // 화면 정렬 또는 기본 정렬
sortOrder: context.sortOrder || "asc", // 화면 정렬 방향 또는 오름차순
});
console.log("📦 API 응답 구조:", {
@ -1763,12 +1814,43 @@ export class ButtonActionExecutor {
const sheetName = config.excelSheetName || "Sheet1";
const includeHeaders = config.excelIncludeHeaders !== false;
// 🆕 컬럼 순서 재정렬 (사용자가 드래그앤드롭으로 변경한 순서 적용)
if (context.columnOrder && context.columnOrder.length > 0 && dataToExport.length > 0) {
console.log("🔄 컬럼 순서 재정렬:", context.columnOrder);
dataToExport = dataToExport.map((row: any) => {
const reorderedRow: any = {};
// 1. columnOrder에 있는 컬럼들을 순서대로 추가
context.columnOrder!.forEach((colName: string) => {
if (colName in row) {
reorderedRow[colName] = row[colName];
}
});
// 2. columnOrder에 없는 나머지 컬럼들 추가 (끝에 배치)
Object.keys(row).forEach((key) => {
if (!(key in reorderedRow)) {
reorderedRow[key] = row[key];
}
});
return reorderedRow;
});
console.log("✅ 컬럼 순서 재정렬 완료:", {
originalColumns: Object.keys(dataToExport[0] || {}),
reorderedColumns: Object.keys(dataToExport[0] || {}),
});
}
console.log("📥 엑셀 다운로드 실행:", {
fileName,
sheetName,
includeHeaders,
dataCount: dataToExport.length,
firstRow: dataToExport[0],
columnOrder: context.columnOrder,
});
// 엑셀 다운로드 실행
@ -1892,6 +1974,177 @@ export class ButtonActionExecutor {
}
}
/**
*
*/
private static async handleCodeMerge(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("🔀 코드 병합 액션 실행:", { config, context });
// 선택된 행 데이터 확인
const selectedRows = context.selectedRowsData || context.flowSelectedData;
if (!selectedRows || selectedRows.length !== 2) {
toast.error("병합할 두 개의 항목을 선택해주세요.");
return false;
}
// 병합할 컬럼명 확인
const columnName = config.mergeColumnName;
if (!columnName) {
toast.error("병합할 컬럼명이 설정되지 않았습니다.");
return false;
}
// 두 개의 선택된 행에서 컬럼 값 추출
const [row1, row2] = selectedRows;
const value1 = row1[columnName];
const value2 = row2[columnName];
if (!value1 || !value2) {
toast.error(`선택한 항목에 "${columnName}" 값이 없습니다.`);
return false;
}
if (value1 === value2) {
toast.error("같은 값은 병합할 수 없습니다.");
return false;
}
// 병합 방향 선택 모달 표시
const confirmed = await new Promise<{ confirmed: boolean; oldValue: string; newValue: string }>((resolve) => {
const modalHtml = `
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;">
<div style="background: white; padding: 24px; border-radius: 8px; max-width: 500px; width: 90%;">
<h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600;"> </h3>
<p style="margin: 0 0 24px 0; color: #666;"> ?</p>
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<button id="merge-option-1" style="flex: 1; padding: 16px; border: 2px solid #e5e7eb; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<div style="font-weight: 600; margin-bottom: 4px;">${value1}</div>
<div style="font-size: 12px; color: #666;"> ${value2} </div>
</button>
<button id="merge-option-2" style="flex: 1; padding: 16px; border: 2px solid #e5e7eb; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<div style="font-weight: 600; margin-bottom: 4px;">${value2}</div>
<div style="font-size: 12px; color: #666;"> ${value1} </div>
</button>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="merge-cancel" style="padding: 8px 16px; border: 1px solid #e5e7eb; border-radius: 6px; background: white; cursor: pointer;"></button>
</div>
</div>
</div>
`;
const modalContainer = document.createElement("div");
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
const option1Btn = modalContainer.querySelector("#merge-option-1") as HTMLButtonElement;
const option2Btn = modalContainer.querySelector("#merge-option-2") as HTMLButtonElement;
const cancelBtn = modalContainer.querySelector("#merge-cancel") as HTMLButtonElement;
// 호버 효과
[option1Btn, option2Btn].forEach((btn) => {
btn.addEventListener("mouseenter", () => {
btn.style.borderColor = "#3b82f6";
btn.style.background = "#eff6ff";
});
btn.addEventListener("mouseleave", () => {
btn.style.borderColor = "#e5e7eb";
btn.style.background = "white";
});
});
option1Btn.addEventListener("click", () => {
document.body.removeChild(modalContainer);
resolve({ confirmed: true, oldValue: value2, newValue: value1 });
});
option2Btn.addEventListener("click", () => {
document.body.removeChild(modalContainer);
resolve({ confirmed: true, oldValue: value1, newValue: value2 });
});
cancelBtn.addEventListener("click", () => {
document.body.removeChild(modalContainer);
resolve({ confirmed: false, oldValue: "", newValue: "" });
});
});
if (!confirmed.confirmed) {
return false;
}
const { oldValue, newValue } = confirmed;
// 미리보기 표시 (옵션)
if (config.mergeShowPreview !== false) {
const { apiClient } = await import("@/lib/api/client");
const previewResponse = await apiClient.post("/code-merge/preview", {
columnName,
oldValue,
});
if (previewResponse.data.success) {
const preview = previewResponse.data.data;
const totalRows = preview.totalAffectedRows;
const confirmMerge = confirm(
`⚠️ 코드 병합 확인\n\n` +
`${oldValue}${newValue}\n\n` +
`영향받는 데이터:\n` +
`- 테이블 수: ${preview.preview.length}\n` +
`- 총 행 수: ${totalRows}\n\n` +
`데이터는 삭제되지 않고, "${columnName}" 컬럼 값만 변경됩니다.\n\n` +
`계속하시겠습니까?`
);
if (!confirmMerge) {
return false;
}
}
}
// 병합 실행
toast.loading("코드 병합 중...", { duration: Infinity });
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.post("/code-merge/merge-all-tables", {
columnName,
oldValue,
newValue,
});
toast.dismiss();
if (response.data.success) {
const data = response.data.data;
toast.success(
`코드 병합 완료!\n` +
`${data.affectedTables.length}개 테이블, ${data.totalRowsUpdated}개 행 업데이트`
);
// 화면 새로고침
context.onRefresh?.();
context.onFlowRefresh?.();
return true;
} else {
toast.error(response.data.message || "코드 병합에 실패했습니다.");
return false;
}
} catch (error: any) {
console.error("❌ 코드 병합 실패:", error);
toast.dismiss();
toast.error(error.response?.data?.message || "코드 병합 중 오류가 발생했습니다.");
return false;
}
}
/**
*
*/
@ -1981,4 +2234,11 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
barcodeFormat: "all",
barcodeAutoSubmit: false,
},
code_merge: {
type: "code_merge",
mergeShowPreview: true,
confirmMessage: "선택한 두 항목을 병합하시겠습니까?",
successMessage: "코드 병합이 완료되었습니다.",
errorMessage: "코드 병합 중 오류가 발생했습니다.",
},
};