반응형
1. App.tsx (메인 컴포넌트)
- React.Component를 상속받는 클래스로 변경
- 상태(state)와 속성(props) 타입 인터페이스 정의
- 생성자에서 초기 상태 설정 및 메소드 바인딩
- 함수형 컴포넌트의 useEffect 대신 componentDidMount 사용
- 상태 업데이트를 위해 this.setState 사용
2. ViewSqlModal.tsx
- 클래스형 컴포넌트로 변환
- 상태와 속성 인터페이스 정의
- 생성자에서 초기 상태와 메소드 바인딩 설정
- 함수형 컴포넌트의 useState 훅 대신 클래스 상태 관리 사용
3. AddPartnerModal.tsx
- 클래스형 컴포넌트로 변환
- react-hook-form 대신 내부 상태로 검색 필드 관리
- 폼 제출 이벤트 핸들러 추가
- 인풋 필드 변경 핸들러 구현
주요 코드 변경 포인트
- 상태 관리 변경:
- 함수형: const [state, setState] = useState(initialValue)
- 클래스형: this.state = { ... } 및 this.setState({ ... })
- 생명주기 메소드:
- 함수형: useEffect(() => {}, [])
- 클래스형: componentDidMount()
- 이벤트 핸들러:
- 함수형: 함수 선언 및 직접 사용
- 클래스형: 메소드 선언, this 컨텍스트 바인딩 필요
- react-hook-form 제거:
- AddPartnerModal에서 react-hook-form 대신 컴포넌트 내부 상태와 이벤트 핸들러로 폼 구현
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById('root')
);
App.tsx
import React, { Component } from 'react';
import { AgGridReact } from 'ag-grid-community';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { ViewSqlModal } from './components/ViewSqlModal';
import { AddPartnerModal } from './components/AddPartnerModal';
import './App.css';
// 메인 그리드 데이터 타입 정의
interface MainRowData {
id: number;
corporateCode: string;
partnerId: string;
partnerName: string;
moduleId: string;
sql: string;
}
interface AppState {
rowData: MainRowData[];
selectedRow: MainRowData | null;
isViewSqlModalOpen: boolean;
isAddModalOpen: boolean;
}
class App extends Component<{}, AppState> {
// 컬럼 정의
columnDefs = [
{ field: 'corporateCode', headerName: '법인', sortable: true, filter: true },
{ field: 'partnerId', headerName: '파트너사 ID', sortable: true, filter: true },
{ field: 'partnerName', headerName: '파트너 명', sortable: true, filter: true },
{ field: 'moduleId', headerName: '모듈 ID', sortable: true, filter: true },
{ field: 'sql', headerName: 'SQL', hide: true }
];
constructor(props: {}) {
super(props);
this.state = {
rowData: [],
selectedRow: null,
isViewSqlModalOpen: false,
isAddModalOpen: false
};
// 메소드 바인딩
this.onSelectionChanged = this.onSelectionChanged.bind(this);
this.handleViewSqlClick = this.handleViewSqlClick.bind(this);
this.handleAddClick = this.handleAddClick.bind(this);
this.handleSqlUpdate = this.handleSqlUpdate.bind(this);
this.handleAddPartners = this.handleAddPartners.bind(this);
}
componentDidMount() {
// 초기 데이터 로드 (실제 환경에서는 API 호출로 대체)
const sampleData: MainRowData[] = [
{ id: 1, corporateCode: 'CORP001', partnerId: 'PID001', partnerName: '파트너사1', moduleId: 'MOD001', sql: 'SELECT * FROM partners WHERE id = 1' },
{ id: 2, corporateCode: 'CORP001', partnerId: 'PID002', partnerName: '파트너사2', moduleId: 'MOD002', sql: 'SELECT * FROM partners WHERE id = 2' },
{ id: 3, corporateCode: 'CORP002', partnerId: 'PID003', partnerName: '파트너사3', moduleId: 'MOD001', sql: 'SELECT * FROM partners WHERE id = 3' },
];
this.setState({ rowData: sampleData });
}
// 행 선택 이벤트 핸들러
onSelectionChanged(event: any) {
const selectedRows = event.api.getSelectedRows();
if (selectedRows.length > 0) {
this.setState({ selectedRow: selectedRows[0] });
} else {
this.setState({ selectedRow: null });
}
}
// View SQL 버튼 클릭 핸들러
handleViewSqlClick() {
if (this.state.selectedRow) {
this.setState({ isViewSqlModalOpen: true });
} else {
alert('먼저 항목을 선택해주세요.');
}
}
// Add 버튼 클릭 핸들러
handleAddClick() {
this.setState({ isAddModalOpen: true });
}
// SQL 업데이트 핸들러 (ViewSqlModal에서 호출)
handleSqlUpdate(updatedSql: string) {
const { rowData, selectedRow } = this.state;
if (selectedRow) {
const updatedRowData = rowData.map(row =>
row.id === selectedRow.id ? { ...row, sql: updatedSql } : row
);
this.setState({
rowData: updatedRowData,
selectedRow: { ...selectedRow, sql: updatedSql },
isViewSqlModalOpen: false
});
}
}
// 새 파트너 추가 핸들러 (AddPartnerModal에서 호출)
handleAddPartners(newPartners: MainRowData[]) {
const { rowData } = this.state;
// 새 ID 생성을 위한 로직
const maxId = Math.max(...rowData.map(row => row.id), 0);
const partnersWithIds = newPartners.map((partner, index) => ({
...partner,
id: maxId + index + 1
}));
this.setState({
rowData: [...rowData, ...partnersWithIds],
isAddModalOpen: false
});
}
render() {
const { rowData, selectedRow, isViewSqlModalOpen, isAddModalOpen } = this.state;
return (
<div className="app-container">
<h1>파트너사 관리</h1>
<div className="button-container">
<button onClick={this.handleViewSqlClick} disabled={!selectedRow}>View SQL</button>
<button onClick={this.handleAddClick}>Add</button>
</div>
<div className="ag-theme-alpine" style={{ height: 500, width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={this.columnDefs}
rowSelection="single"
onSelectionChanged={this.onSelectionChanged}
pagination={true}
paginationPageSize={10}
/>
</div>
{isViewSqlModalOpen && selectedRow && (
<ViewSqlModal
isOpen={isViewSqlModalOpen}
onClose={() => this.setState({ isViewSqlModalOpen: false })}
sql={selectedRow.sql}
onSave={this.handleSqlUpdate}
/>
)}
{isAddModalOpen && (
<AddPartnerModal
isOpen={isAddModalOpen}
onClose={() => this.setState({ isAddModalOpen: false })}
onAdd={this.handleAddPartners}
/>
)}
</div>
);
}
}
export default App;
ViewSqlModal.tsx
import React, { Component } from 'react';
import './Modal.css';
interface ViewSqlModalProps {
isOpen: boolean;
onClose: () => void;
sql: string;
onSave: (updatedSql: string) => void;
}
interface ViewSqlModalState {
sqlText: string;
checkResult: string | null;
isLoading: boolean;
}
export class ViewSqlModal extends Component<ViewSqlModalProps, ViewSqlModalState> {
constructor(props: ViewSqlModalProps) {
super(props);
this.state = {
sqlText: props.sql,
checkResult: null,
isLoading: false
};
// 메소드 바인딩
this.handleSqlChange = this.handleSqlChange.bind(this);
this.handleCheckSql = this.handleCheckSql.bind(this);
this.handleOk = this.handleOk.bind(this);
}
// SQL 텍스트 변경 핸들러
handleSqlChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
this.setState({
sqlText: e.target.value,
checkResult: null // 변경 시 체크 결과 초기화
});
}
// SQL 유효성 체크 함수
async handleCheckSql() {
try {
this.setState({
isLoading: true,
checkResult: null
});
// 실제 환경에서는 아래 코드를 서버 API 호출로 대체
// const response = await fetch('/api/check-sql', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ sql: this.state.sqlText })
// });
// const data = await response.json();
// 테스트를 위한 목업 응답
await new Promise(resolve => setTimeout(resolve, 1000)); // 서버 호출 시뮬레이션
// 임시 검증 로직 (실제로는 서버에서 처리)
const isValid = this.state.sqlText.trim().toUpperCase().startsWith('SELECT');
if (isValid) {
this.setState({ checkResult: "SQL 문법이 유효합니다." });
} else {
this.setState({ checkResult: "SQL 문법 오류: SELECT 문으로 시작해야 합니다." });
}
} catch (error) {
this.setState({ checkResult: "SQL 검증 중 오류가 발생했습니다." });
console.error("SQL 검증 오류:", error);
} finally {
this.setState({ isLoading: false });
}
}
// OK 버튼 핸들러 - 변경된 SQL을 저장
handleOk() {
this.props.onSave(this.state.sqlText);
}
render() {
const { isOpen, onClose } = this.props;
const { sqlText, checkResult, isLoading } = this.state;
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h2>View SQL</h2>
</div>
<div className="modal-body">
<textarea
value={sqlText}
onChange={this.handleSqlChange}
rows={10}
className="sql-textarea"
/>
{checkResult && (
<div className={`check-result ${checkResult.includes('오류') ? 'error' : 'success'}`}>
{checkResult}
</div>
)}
</div>
<div className="modal-footer">
<button
onClick={this.handleCheckSql}
disabled={isLoading}
className="btn-check"
>
{isLoading ? '검증 중...' : 'Check SQL'}
</button>
<button onClick={this.handleOk} className="btn-ok">OK</button>
<button onClick={onClose} className="btn-close">Close</button>
</div>
</div>
</div>
);
}
}
AddPartnerModal.tsx
import React, { Component } from 'react';
import { AgGridReact } from 'ag-grid-community';
import { useForm } from 'react-hook-form';
import './Modal.css';
interface Partner {
corporateCode: string;
partnerId: string;
partnerName: string;
moduleId: string;
sql: string;
}
interface AddPartnerModalProps {
isOpen: boolean;
onClose: () => void;
onAdd: (partners: Partner[]) => void;
}
interface AddPartnerModalState {
rowData: Partner[];
isLoading: boolean;
selectedRows: Partner[];
searchValues: {
corporateCode: string;
partnerId: string;
partnerName: string;
};
}
// 클래스형 컴포넌트는 react-hook-form을 직접 사용할 수 없으므로
// 검색 필드를 내부 상태로 관리합니다.
export class AddPartnerModal extends Component<AddPartnerModalProps, AddPartnerModalState> {
// 컬럼 정의
columnDefs = [
{
field: 'checkBox',
headerName: '',
checkboxSelection: true,
headerCheckboxSelection: true,
width: 50
},
{ field: 'corporateCode', headerName: '법인', sortable: true, filter: true },
{ field: 'partnerId', headerName: '파트너사 ID', sortable: true, filter: true },
{ field: 'partnerName', headerName: '파트너 명', sortable: true, filter: true },
{ field: 'moduleId', headerName: '모듈 ID', sortable: true, filter: true }
];
constructor(props: AddPartnerModalProps) {
super(props);
this.state = {
rowData: [],
isLoading: false,
selectedRows: [],
searchValues: {
corporateCode: '',
partnerId: '',
partnerName: ''
}
};
// 메소드 바인딩
this.onSearch = this.onSearch.bind(this);
this.onSelectionChanged = this.onSelectionChanged.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
// 인풋 필드 변경 핸들러
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const { name, value } = e.target;
this.setState(prevState => ({
searchValues: {
...prevState.searchValues,
[name]: value
}
}));
}
// 폼 제출 및 조회 핸들러
async onSearch(e: React.FormEvent) {
e.preventDefault();
const { searchValues } = this.state;
try {
this.setState({ isLoading: true });
// 실제 환경에서는 아래 코드를 서버 API 호출로 대체
// const response = await fetch('/api/search-partners', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(searchValues)
// });
// const responseData = await response.json();
// 테스트를 위한 목업 응답
await new Promise(resolve => setTimeout(resolve, 1000)); // 서버 호출 시뮬레이션
// 필터링된 목업 데이터
const mockData: Partner[] = [
{ corporateCode: 'CORP001', partnerId: 'PID101', partnerName: '테스트파트너1', moduleId: 'MOD001', sql: 'SELECT * FROM default_table WHERE id = 1' },
{ corporateCode: 'CORP001', partnerId: 'PID102', partnerName: '테스트파트너2', moduleId: 'MOD002', sql: 'SELECT * FROM default_table WHERE id = 2' },
{ corporateCode: 'CORP002', partnerId: 'PID201', partnerName: '테스트파트너3', moduleId: 'MOD001', sql: 'SELECT * FROM default_table WHERE id = 3' },
{ corporateCode: 'CORP002', partnerId: 'PID202', partnerName: '테스트파트너4', moduleId: 'MOD003', sql: 'SELECT * FROM default_table WHERE id = 4' },
{ corporateCode: 'CORP003', partnerId: 'PID301', partnerName: '테스트파트너5', moduleId: 'MOD002', sql: 'SELECT * FROM default_table WHERE id = 5' },
];
// 검색 조건에 따른 필터링
const filteredData = mockData.filter(item => {
return (
(!searchValues.corporateCode || item.corporateCode.includes(searchValues.corporateCode)) &&
(!searchValues.partnerId || item.partnerId.includes(searchValues.partnerId)) &&
(!searchValues.partnerName || item.partnerName.includes(searchValues.partnerName))
);
});
this.setState({ rowData: filteredData });
} catch (error) {
console.error("파트너 검색 오류:", error);
alert("파트너 검색 중 오류가 발생했습니다.");
} finally {
this.setState({ isLoading: false });
}
}
// 행 선택 변경 이벤트 핸들러
onSelectionChanged(event: any) {
const selectedNodes = event.api.getSelectedNodes();
const selectedData = selectedNodes.map((node: any) => node.data);
this.setState({ selectedRows: selectedData });
}
// OK 버튼 핸들러
handleOk() {
const { selectedRows } = this.state;
if (selectedRows.length === 0) {
alert('추가할 항목을 선택해주세요.');
return;
}
this.props.onAdd(selectedRows);
}
render() {
const { isOpen, onClose } = this.props;
const { rowData, isLoading, searchValues } = this.state;
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content add-partner-modal">
<div className="modal-header">
<h2>파트너 추가</h2>
</div>
<div className="modal-body">
<form onSubmit={this.onSearch} className="search-form">
<div className="form-group">
<label>법인</label>
<input
type="text"
name="corporateCode"
value={searchValues.corporateCode}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label>파트너사 ID</label>
<input
type="text"
name="partnerId"
value={searchValues.partnerId}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label>파트너 명</label>
<input
type="text"
name="partnerName"
value={searchValues.partnerName}
onChange={this.handleInputChange}
/>
</div>
<button type="submit" disabled={isLoading} className="btn-search">
{isLoading ? '조회 중...' : '조회'}
</button>
</form>
<div className="ag-theme-alpine" style={{ height: 300, width: '100%', marginTop: '20px' }}>
<AgGridReact
rowData={rowData}
columnDefs={this.columnDefs}
rowSelection="multiple"
onSelectionChanged={this.onSelectionChanged}
pagination={true}
paginationPageSize={5}
/>
</div>
</div>
<div className="modal-footer">
<button onClick={this.handleOk} className="btn-ok">OK</button>
<button onClick={onClose} className="btn-close">Close</button>
</div>
</div>
</div>
);
}
}
반응형