본문 바로가기

카테고리 없음

React 17.0.2 모달 창 띄우기(클래스형)

반응형

1. App.tsx (메인 컴포넌트)

  • React.Component를 상속받는 클래스로 변경
  • 상태(state)와 속성(props) 타입 인터페이스 정의
  • 생성자에서 초기 상태 설정 및 메소드 바인딩
  • 함수형 컴포넌트의 useEffect 대신 componentDidMount 사용
  • 상태 업데이트를 위해 this.setState 사용

2. ViewSqlModal.tsx

  • 클래스형 컴포넌트로 변환
  • 상태와 속성 인터페이스 정의
  • 생성자에서 초기 상태와 메소드 바인딩 설정
  • 함수형 컴포넌트의 useState 훅 대신 클래스 상태 관리 사용

3. AddPartnerModal.tsx

  • 클래스형 컴포넌트로 변환
  • react-hook-form 대신 내부 상태로 검색 필드 관리
  • 폼 제출 이벤트 핸들러 추가
  • 인풋 필드 변경 핸들러 구현

주요 코드 변경 포인트

  1. 상태 관리 변경:
    • 함수형: const [state, setState] = useState(initialValue)
    • 클래스형: this.state = { ... } 및 this.setState({ ... })
  2. 생명주기 메소드:
    • 함수형: useEffect(() => {}, [])
    • 클래스형: componentDidMount()
  3. 이벤트 핸들러:
    • 함수형: 함수 선언 및 직접 사용
    • 클래스형: 메소드 선언, this 컨텍스트 바인딩 필요
  4. 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>
    );
  }
}



 

반응형