- ag-grid와 React 17 클래스 컴포넌트 환경에서 타입스크립트로 구현
- 체크박스 행 선택, SQL 편집 모달, 서버 연동
- 데이터 타입과 모달 컴포넌트를 분리하고, 서버 연동 시 기본값 설정 및 LIMIT 1 추가 로직까지 고려
1. 데이터 타입 정의 (types/gridTypes.ts)
// types/gridTypes.ts
export interface GridRowData {
[key: string]: any; // 실제 데이터 필드에 따라 구체적인 타입 정의 필요
sql?: string;
}
export interface ModalState {
isOpen: boolean;
selectedRow?: GridRowData;
editedSql: string;
isCheckSqlSuccess: boolean | null;
checkSqlMessage: string;
}
2. SQL 편집 모달 컴포넌트 (components/SqlEditModal.tsx)
// components/SqlEditModal.tsx
import React, { Component } from 'react';
import { ModalState, GridRowData } from '../types/gridTypes';
interface Props {
modalState: ModalState;
onClose: () => void;
onApply: (updatedRow: GridRowData) => void;
onCheckSql: (sql: string) => Promise<{ success: boolean; message: string }>;
}
interface State {
editedSql: string;
isCheckSqlSuccess: boolean | null;
checkSqlMessage: string;
}
class SqlEditModal extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
editedSql: props.modalState.selectedRow?.sql || '',
isCheckSqlSuccess: null,
checkSqlMessage: '',
};
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (nextProps.modalState.isOpen && nextProps.modalState.selectedRow?.sql !== prevState.editedSql) {
return {
editedSql: nextProps.modalState.selectedRow?.sql || '',
isCheckSqlSuccess: null,
checkSqlMessage: '',
};
}
return null;
}
handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({ editedSql: event.target.value, isCheckSqlSuccess: null, checkSqlMessage: '' });
};
handleCheckSqlClick = async () => {
const { onCheckSql } = this.props;
const { editedSql } = this.state;
const result = await onCheckSql(editedSql);
this.setState({ isCheckSqlSuccess: result.success, checkSqlMessage: result.message });
};
handleApplyClick = () => {
const { onApply, modalState } = this.props;
const { editedSql } = this.state;
if (modalState.selectedRow) {
onApply({ ...modalState.selectedRow, sql: editedSql });
}
this.props.onClose();
};
render() {
const { modalState, onClose } = this.props;
const { editedSql, isCheckSqlSuccess, checkSqlMessage } = this.state;
if (!modalState.isOpen || !modalState.selectedRow) {
return null;
}
return (
<div style={modalStyles.overlay}>
<div style={modalStyles.container}>
<h2 style={modalStyles.title}>SQL 편집</h2>
<label htmlFor="checkSql" style={modalStyles.label}>Check SQL</label>
<textarea
id="checkSql"
style={modalStyles.textarea}
value={editedSql}
onChange={this.handleInputChange}
/>
<div style={modalStyles.buttonGroup}>
<button style={modalStyles.checkSqlButton} onClick={this.handleCheckSqlClick}>
Check SQL
</button>
{checkSqlMessage && (
<div style={isCheckSqlSuccess ? modalStyles.successMessage : modalStyles.errorMessage}>
{checkSqlMessage}
</div>
)}
</div>
<div style={modalStyles.buttonGroup}>
<button
style={modalStyles.confirmButton}
onClick={this.handleApplyClick}
disabled={isCheckSqlSuccess !== true}
>
확인
</button>
<button style={modalStyles.cancelButton} onClick={onClose}>
취소
</button>
</div>
</div>
</div>
);
}
}
const modalStyles = {
overlay: {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
container: {
backgroundColor: '#fff',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
width: '50%',
maxWidth: '600px',
},
title: {
fontSize: '20px',
marginBottom: '15px',
},
label: {
display: 'block',
marginBottom: '5px',
fontWeight: 'bold',
},
textarea: {
width: '100%',
padding: '10px',
marginBottom: '15px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px',
minHeight: '100px',
},
buttonGroup: {
display: 'flex',
justifyContent: 'flex-end',
gap: '10px',
marginBottom: '10px',
alignItems: 'center',
},
checkSqlButton: {
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '8px 15px',
cursor: 'pointer',
fontSize: '14px',
},
confirmButton: {
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '8px 15px',
cursor: 'pointer',
fontSize: '14px',
},
cancelButton: {
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '8px 15px',
cursor: 'pointer',
fontSize: '14px',
},
successMessage: {
color: 'green',
marginLeft: '10px',
},
errorMessage: {
color: 'red',
marginLeft: '10px',
},
};
export default SqlEditModal;
3. ag-grid를 사용하는 React 클래스 컴포넌트 (MyGridComponent.tsx)
// MyGridComponent.tsx
import React, { Component } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { ColDef, GridReadyEvent, RowSelectedEvent } from 'ag-grid-community';
import { GridRowData, ModalState } from './types/gridTypes';
import SqlEditModal from './components/SqlEditModal';
interface Props {}
interface State {
columnDefs: ColDef[];
rowData: GridRowData[];
selectedRows: GridRowData[];
modalState: ModalState;
}
class MyGridComponent extends Component<Props, State> {
private gridApi: any;
private gridColumnApi: any;
constructor(props: Props) {
super(props);
this.state = {
columnDefs: [
{ headerName: '선택', field: 'isSelected', headerCheckboxSelection: true, checkboxSelection: true, width: 80 },
{ headerName: 'ID', field: 'id' },
{ headerName: '이름', field: 'name' },
{ headerName: 'SQL', field: 'sql' },
// ... 다른 컬럼 정의
],
rowData: [
{ id: 1, name: '데이터 1', sql: 'SELECT * FROM table1 WHERE country = ${countryCode}' },
{ id: 2, name: '데이터 2', sql: 'SELECT col1, col2 FROM table2 WHERE partner = ${partnerCode} LIMIT 10' },
{ id: 3, name: '데이터 3', sql: 'SELECT order_id FROM orders WHERE order_date BETWEEN ${startDate} AND ${endDate}' },
// ... 더 많은 데이터
],
selectedRows: [],
modalState: {
isOpen: false,
selectedRow: undefined,
editedSql: '',
isCheckSqlSuccess: null,
checkSqlMessage: '',
},
};
}
onGridReady = (params: GridReadyEvent) => {
this.gridApi = params.api;
this.gridColumnApi = params.columnApi;
};
onRowSelected = (event: RowSelectedEvent) => {
const selectedNodes = this.gridApi.getSelectedNodes();
this.setState({
selectedRows: selectedNodes.map((node: any) => node.data),
});
};
handleCheckSqlButtonClick = () => {
const { selectedRows } = this.state;
if (selectedRows.length > 0) {
this.setState({
modalState: {
isOpen: true,
selectedRow: selectedRows[0],
editedSql: selectedRows[0].sql || '',
isCheckSqlSuccess: null,
checkSqlMessage: '',
},
});
} else {
alert('선택된 행이 없습니다.');
}
};
closeModal = () => {
this.setState({
modalState: {
...this.state.modalState,
isOpen: false,
selectedRow: undefined,
editedSql: '',
isCheckSqlSuccess: null,
checkSqlMessage: '',
},
});
};
applyEditedSql = (updatedRow: GridRowData) => {
const { rowData } = this.state;
const updatedRowData = rowData.map((row) =>
row.id === updatedRow.id ? updatedRow : row
);
this.setState({ rowData: updatedRowData });
};
checkSqlToServer = async (sql: string) => {
// limit 또는 LIMIT 가 없으면 마지막 줄에 LIMIT 1 추가
const sqlWithLimit = !/(limit|LIMIT)\s+\d+$/.test(sql.trim()) ? `${sql.trim()}\nLIMIT 1` : sql;
// 기본 변수 값 설정
const processedSql = sqlWithLimit
.replace(/\${countryCode}/g, 'KR')
.replace(/\${partnerCode}/g, ' Walmart')
.replace(/\${startDate}/g, this.formatDate(this.addDays(new Date(), -10)))
.replace(/\${endDate}/g, this.formatDate(new Date()));
try {
const response = await fetch('http://127.0.0.1:3060/viw/erp/checkSql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ sql: processedSql }),
});
if (!response.ok) {
const errorData = await response.json();
return { success: false, message: errorData.message || `HTTP error! status: ${response.status}` };
}
const result = await response.json();
return { success: result.success, message: result.message };
} catch (error: any) {
return { success: false, message: error.message || 'SQL 체크 중 오류 발생' };
}
};
addDays = (date: Date, days: number): Date => {
const newDate = new Date(date);
newDate.setDate(date.getDate() + days);
return newDate;
};
formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
render() {
const { columnDefs, rowData, modalState } = this.state;
return (
<div className="ag-theme-alpine" style={{ height: 400, width: '100%' }}>
<AgGridReact
columnDefs={columnDefs}
rowData={rowData}
rowSelection="multiple"
onGridReady={this.onGridReady}
onRowSelected={this.onRowSelected}
/>
<button onClick={this.handleCheckSqlButtonClick} style={{ marginTop: '10px' }}>
CheckSQL
</button>
{modalState.isOpen && modalState.selectedRow && (
<SqlEditModal
modalState={modalState}
onClose={this.closeModal}
onApply={this.applyEditedSql}
onCheckSql={this.checkSqlToServer}
/>
)}
</div>
);
}
}
export default MyGridComponent;
4. Java 백엔드 코드 (예시)
// CheckSqlController.java
package com.example.erp.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.erp.service.CheckSqlService;
@Controller
@RequestMapping("/viw/erp")
public class CheckSqlController {
@Autowired
private CheckSqlService checkSqlService;
@PostMapping("/checkSql")
@ResponseBody
public ResponseEntity<Map<String, Object>> checkSql(@RequestBody Map<String, String> payload) {
String sql = payload.get("sql");
Map<String, Object> result = new HashMap<>();
try {
boolean isSuccess = checkSqlService.executeSql(sql);
result.put("success", isSuccess);
result.put("message", "SQL 검증 완료.");
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (Exception e) {
result.put("success", false);
result.put("message", "SQL 실행 오류: " + e.getMessage());
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
// CheckSqlService.java
package com.example.erp.service;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CheckSqlService {
@Autowired
private SqlSession sqlSession;
@Transactional
public boolean executeSql(String sql) {
try {
// ibatis를 사용하여 SQL 실행 (예시)
// 실제 ibatis 설정 및 매퍼에 따라 namespace와 id를 변경해야 합니다.
sqlSession.selectList("com.example.erp.mapper.SqlMapper.executeCheckSql", sql);
return true;
} catch (Exception e) {
// SQL 실행 오류 발생
throw e;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.erp.mapper.SqlMapper">
<select id="executeCheckSql" parameterType="java.lang.String">
${value}
</select>
</mapper>
설명:
- types/gridTypes.ts: ag-grid의 행 데이터와 모달 상태 관리를 위한 인터페이스를 정의합니다.
ag-grid를 사용하는 React 17버전 클래스 컴포넌트 환경에서 타입스크립트를 기반으로,
체크박스로 행을 선택한 후 CheckSQL 버튼 클릭 시 첫 번째 선택된 행의 SQL 값을 별도의 모달 창에서 편집하고 확인 버튼을 통해 선택 된 행의 필드에 적용하는 기능을, 데이터 타입 및 모달 컴포넌트를 분리하여 구현하고
모달 창에서는 타이틀, 텍스트 박스(CheckSQL), Check SQL 버튼, 확인 버튼, 취소 버튼 으로 구성하고
Check SQL 버튼 클릭하면 Java, Spring, ibatis, Mariadb로 구성 된 웹서버(http://127.0.0.1:3060/viw/erp/checkSql)를 호출하여 SQL 체크 수행 결과 정상 수행되었으면 정상처리 메시지 출력하고, 오류 또는 예외사항 발생 했을 경우 해당 메시지 출력, CheckSQL 결과가 오류 또는 예외사항 발생 했을 경우 확인 버튼 disable 처리하고 정상 수행되었을 경우 enable 처리, Java에서 CheckSQL 의 변수 항목으로 정의한 SQL 문장에서 Default 값으로 CheckSQL 문장에서 ${countryCode} 는 'KR' 로 세팅하고 ${partnerCode} 는 ' Walmart' 세팅하고 ${startDate} 는 10일 전 일자 세팅하고 ${endDate} 는 오늘 일자 세팅하여 수행 합니다.
CheckSQL 문장에서 limit 또는 LIMIT 가 없으면 마지막 줄에 LIMIT 1를 추가 합니다.
React는 클래스 컴포넌트 형태로 코드 만들고 Java, ibasts 비지니스 코드도 만들어 주세요.
'개발자정보' 카테고리의 다른 글
윈도우에 Apache Maven을 설치하는 방법 (0) | 2025.05.04 |
---|---|
React의 Material UI에서 Dialog 컴포넌트 사용 시 PaperComponent prop (0) | 2025.05.02 |
React와 Spring Boot를 사용한 SQL 체크 및 모달 창 구현 (0) | 2025.05.02 |
React17, Typescript 환경에서 modal 띄우는 예제 (0) | 2025.05.02 |
Windows에 Python 설치하기 (0) | 2025.04.27 |