본문 바로가기

개발자정보

React17, Typescript 환경에서 modal 띄우는 예제

반응형

 

 

중간정리

ag-grid를 사용하는 React 17버전 클래스 컴포넌트 환경에서 타입스크립트를 기반으로,

체크박스로 행을 선택한 후 CheckSQL 버튼 클릭 시 첫 번째 선택된 행의 SQL 값을 별도의 모달 창에서 편집하고 확인 버튼을 통해 선택 된 행의 필드에 적용하는 기능을, 데이터 타입 및 모달 컴포넌트를 분리하여 구현하고 클래스 컴포넌트 형태로 코드 만들어 주세요.

 

Class Components

1. RowDataType.ts 파일 (변경 없음)

// src/types/RowDataType.ts (예시 경로)

export interface RowDataType {
  id: number; // 각 행을 고유하게 식별할 ID (업데이트 시 필요)
  make: string;
  model: string;
  price: number;
  sql: string; // 사용자의 요청에 따른 SQL 필드
}

 

2. SqlEditModal.tsx 파일 (변경 없음 - 함수형 컴포넌트로 유지)

간단한 모달 컴포넌트는 함수형으로 유지하는 것이 일반적이며 관리하기 용이합니다.

// src/components/SqlEditModal.tsx (예시 경로)

import React, { useState, useEffect, useCallback } from 'react';

interface SqlEditModalProps {
  isOpen: boolean;
  title: string;
  initialValue: string; // 모달이 열릴 때 초기 SQL 값
  onOk: (editedValue: string) => void; // OK 버튼 클릭 시 호출될 함수, 편집된 값 전달
  onCancel: () => void; // Cancel 버튼 클릭 시 호출될 함수
}

const SqlEditModal: React.FC<SqlEditModalProps> = ({
  isOpen,
  title,
  initialValue,
  onOk,
  onCancel,
}) => {
  // 모달 내부에서 관리할 SQL 값 상태
  const [editedSqlValue, setEditedSqlValue] = useState<string>(initialValue);

  // 모달이 열릴 때마다 초기 값으로 상태 업데이트
  useEffect(() => {
    setEditedSqlValue(initialValue);
  }, [initialValue, isOpen]); // initialValue 또는 isOpen이 변경될 때만 실행

  // 입력 필드 값 변경 핸들러
  const handleInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    setEditedSqlValue(event.target.value);
  }, []);

  // OK 버튼 클릭 핸들러: 편집된 값을 부모 컴포넌트로 전달
  const handleOkClick = useCallback(() => {
    onOk(editedSqlValue);
  }, [onOk, editedSqlValue]);

  // Cancel 버튼 클릭 핸들러: 취소 동작 수행
  const handleCancelClick = useCallback(() => {
    onCancel();
  }, [onCancel]);

  // 모달이 열려 있지 않으면 아무것도 렌더링하지 않음
  if (!isOpen) {
    return null;
  }

  // 모달 스타일 (간단한 예시)
  const modalStyle: React.CSSProperties = {
    position: 'fixed',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    backgroundColor: '#fff',
    padding: '20px',
    zIndex: 1000,
    boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
    borderRadius: '8px',
    width: '400px',
    maxWidth: '90%',
    display: 'flex',
    flexDirection: 'column',
  };

  const backdropStyle: React.CSSProperties = {
    position: 'fixed',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    zIndex: 999,
  };

  const inputStyle: React.CSSProperties = {
    width: '100%', // 부모 요소 너비에 맞춤
    marginBottom: '15px',
    padding: '8px',
    boxSizing: 'border-box',
    marginTop: '10px',
  };

  const buttonContainerStyle: React.CSSProperties = {
    display: 'flex',
    justifyContent: 'flex-end',
    gap: '10px', // 버튼 사이 간격
  };

  const buttonStyle: React.CSSProperties = {
     padding: '8px 15px',
     cursor: 'pointer',
     border: '1px solid #ccc',
     borderRadius: '4px',
     backgroundColor: '#eee',
  }

  const okButtonStyle: React.CSSProperties = {
    ...buttonStyle,
    backgroundColor: '#007bff',
    color: 'white',
    border: 'none',
  }


  return (
    <>
      {/* 배경 오버레이 */}
      <div style={backdropStyle} onClick={handleCancelClick}></div>
      {/* 실제 모달 내용 */}
      <div style={modalStyle}>
        <h3 style={{ marginTop: 0, marginBottom: '5px' }}>{title}</h3>
        {/* SQL 값 편집을 위한 텍스트 영역이나 입력 필드 */}
        <input
          type="text"
          value={editedSqlValue}
          onChange={handleInputChange}
          style={inputStyle}
        />
        <div style={buttonContainerStyle}>
          <button onClick={handleOkClick} style={okButtonStyle}>OK</button>
          <button onClick={handleCancelClick} style={buttonStyle}>Cancel</button>
        </div>
      </div>
    </>
  );
};

export default SqlEditModal;

 

3. GridWithSqlEdit.tsx 파일 (클래스 컴포넌트로 변경)

// src/components/GridWithSqlEdit.tsx (예시 경로)

import React, { Component, createRef } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { GridApi, GridReadyEvent, ColDef } from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css'; // 사용할 테마 CSS를 선택하세요

// 별도로 정의한 데이터 타입 임포트
import { RowDataType } from '../types/RowDataType'; // 실제 파일 경로에 맞게 수정하세요

// 별도로 정의한 모달 컴포넌트 임포트
import SqlEditModal from './SqlEditModal'; // 실제 파일 경로에 맞게 수정하세요

// 컴포넌트 상태 타입 정의
interface GridWithSqlEditState {
  rowData: RowDataType[];
  isModalOpen: boolean;
  sqlValueForModal: string; // 모달에 전달할 초기 SQL 값
  isEditButtonEnabled: boolean; // SQL 편집 버튼 활성화 상태
}

class GridWithSqlEdit extends Component<{}, GridWithSqlEditState> {

  // ag-grid API에 접근하기 위한 Ref
  private gridApi = createRef<GridApi<RowDataType>>();

  // 컬럼 정의 (클래스 속성으로 정의)
  private columnDefs: ColDef[] = [
    {
      field: 'make',
      headerName: '제조사',
      checkboxSelection: true, // 이 컬럼에 체크박스 표시
      headerCheckboxSelection: true, // 헤더에 전체 선택 체크박스 표시
    },
    { field: 'model', headerName: '모델명' },
    { field: 'price', headerName: '가격' },
    { field: 'sql', headerName: 'SQL', editable: false }, // SQL 필드 (그리드 내에서는 직접 편집 방지)
  ];

  // 기본 컬럼 속성 정의 (클래스 속성으로 정의)
  private defaultColDef: ColDef = {
    flex: 1,
    minWidth: 100,
    sortable: true,
    filter: true,
    resizable: true, // 컬럼 크기 조절 가능
  };

  // 초기 상태 정의
  public state: GridWithSqlEditState = {
    rowData: [
      { id: 1, make: "Toyota", model: "Celica", price: 35000, sql: "SELECT * FROM cars WHERE id = 1" },
      { id: 2, make: "Ford", model: "Mondeo", price: 32000, sql: "SELECT user_name FROM users WHERE status = 'active'" },
      { id: 3, make: "Porsche", model: "Boxster", price: 72000, sql: "SELECT count(*) FROM orders WHERE order_date > '2023-01-01'" },
      { id: 4, make: "BMW", model: "M5", price: 60000, sql: "SELECT product_name, price FROM products WHERE category = 'electronics'" },
      { id: 5, make: "Audi", model: "A4", price: 40000, sql: "SELECT inventory_level FROM warehouse WHERE location = 'A'" },
    ],
    isModalOpen: false,
    sqlValueForModal: '',
    isEditButtonEnabled: false, // 초기에는 버튼 비활성화
  };

  // 그리드 준비 완료 시 호출되는 메서드
  // 화살표 함수로 정의하여 'this' 바인딩 문제를 회피
  onGridReady = (params: GridReadyEvent) => {
    // this.gridApi.current에 그리드 API 객체 할당
    if (this.gridApi.current) {
       // createRef를 사용한 경우 params.api를 ref의 current 속성에 바로 할당할 필요는 없습니다.
       // AgGridReact 컴포넌트에 ref={this.gridApi}로 연결하면 자동으로 할당됩니다.
       // 여기서는 추가적인 초기화 작업만 수행합니다.
       console.log("Ag-Grid is ready.");
    }
  };

  // 행 선택 상태 변경 시 호출되는 메서드
  onSelectionChanged = () => {
    if (!this.gridApi.current) return;

    // 현재 선택된 행 노드들을 가져옵니다.
    const selectedNodes = this.gridApi.current.api.getSelectedNodes();

    // 선택된 행이 1개 이상인지 확인하여 버튼 활성화 상태 업데이트
    this.setState({ isEditButtonEnabled: selectedNodes.length > 0 });
  };

  // '선택된 SQL 수정' 버튼 클릭 핸들러
  handleEditSelectedSqlClick = () => {
    if (!this.gridApi.current) return;

    const selectedNodes = this.gridApi.current.api.getSelectedNodes();

    // 선택된 행이 하나 이상인 경우에만 모달 열기 로직 실행
    if (selectedNodes.length > 0) {
      // 첫 번째 선택된 행의 SQL 값을 가져와 모달에 전달할 상태에 저장
      const firstSelectedSql = selectedNodes[0]?.data?.sql || '';
      this.setState({
        sqlValueForModal: firstSelectedSql,
        isModalOpen: true, // 모달 열기
      });
    } else {
      // 선택된 행이 없으면 버튼 비활성화 상태 유지 (onSelectionChanged에서 처리됨)
      console.log("수정할 행을 선택해주세요.");
    }
  };

  // 모달에서 'OK' 버튼 클릭 시 호출될 콜백 (모달로부터 편집된 값 전달받음)
  handleModalOk = (editedValue: string) => {
    if (!this.gridApi.current) return;

    const selectedNodes = this.gridApi.current.api.getSelectedNodes();
    const updatedRows: RowDataType[] = [];

    // 모달에서 전달받은 편집된 값으로 선택된 모든 행의 SQL 필드를 업데이트
    selectedNodes.forEach(node => {
      if (node.data) {
        const updatedRow = { ...node.data, sql: editedValue };
        updatedRows.push(updatedRow);
      }
    });

    // applyTransaction을 사용하여 그리드 데이터 업데이트
    this.gridApi.current.api.applyTransaction({ update: updatedRows });

    // 모달 닫기 및 상태 초기화, 선택 해제
    this.setState({
      isModalOpen: false,
      sqlValueForModal: '', // 모달에 전달했던 SQL 값 상태 초기화
    });
    this.gridApi.current.api.deselectAll(); // 선택 해제
  };

  // 모달에서 'Cancel' 버튼 클릭 시 호출될 콜백
  handleModalCancel = () => {
    // 모달 닫기 및 상태 초기화, 선택 해제
    this.setState({
      isModalOpen: false,
      sqlValueForModal: '', // 모달에 전달했던 SQL 값 상태 초기화
    });
     if (this.gridApi.current) {
        this.gridApi.current.api.deselectAll(); // 선택 해제
     }
  };

  render() {
    const { rowData, isModalOpen, sqlValueForModal, isEditButtonEnabled } = this.state;

    return (
      <div style={{ height: 450, width: '100%', display: 'flex', flexDirection: 'column' }}> {/* 전체 컨테이너 스타일 */}
        {/* SQL 수정 버튼 */}
        <div style={{ marginBottom: '10px' }}>
          <button
            onClick={this.handleEditSelectedSqlClick} // 클래스 메서드 호출
            disabled={!isEditButtonEnabled} // 선택된 행이 없으면 버튼 비활성화
            style={{ padding: '8px 15px', cursor: isEditButtonEnabled ? 'pointer' : 'not-allowed' }}
          >
            선택된 SQL 수정
          </button>
        </div>

        {/* ag-grid 컨테이너 */}
        <div className="ag-theme-alpine" style={{ flexGrow: 1, width: '100%' }}> {/* 남은 공간을 모두 사용하도록 flexGrow 설정 */}
          <AgGridReact<RowDataType>
            ref={this.gridApi} // createRef로 생성한 ref 연결
            rowData={rowData} // 상태에서 데이터 가져오기
            columnDefs={this.columnDefs} // 클래스 속성 사용
            defaultColDef={this.defaultColDef} // 클래스 속성 사용
            rowSelection="multiple"
            onGridReady={this.onGridReady} // 클래스 메서드 연결
            onSelectionChanged={this.onSelectionChanged} // 클래스 메서드 연결
          />
        </div>

        {/* SQL 편집 모달 컴포넌트 */}
        <SqlEditModal
          isOpen={isModalOpen} // 상태에서 값 전달
          title="선택된 행의 SQL 값 수정"
          initialValue={sqlValueForModal} // 상태에서 값 전달
          onOk={this.handleModalOk} // 클래스 메서드 전달
          onCancel={this.handleModalCancel} // 클래스 메서드 전달
        />
      </div>
    );
  }
}

export default GridWithSqlEdit;

 

 

 

 

해당 model 관련 된 것은 별도 ts 파일로 분리하고 버튼을 클릭 했을 때 SQL 값을 전달하고 OK 했을 떄 SQL를 리턴 받아 적용하는 방식으로 변경 해 주세요

RowDataType.ts

// src/types/RowDataType.ts (예시 경로)

export interface RowDataType {
  id: number; // 각 행을 고유하게 식별할 ID (업데이트 시 필요)
  make: string;
  model: string;
  price: number;
  sql: string; // 사용자의 요청에 따른 SQL 필드
}

 

SqlEditModal.tsx

// src/components/SqlEditModal.tsx (예시 경로)

import React, { useState, useEffect, useCallback } from 'react';

interface SqlEditModalProps {
  isOpen: boolean;
  title: string;
  initialValue: string; // 모달이 열릴 때 초기 SQL 값
  onOk: (editedValue: string) => void; // OK 버튼 클릭 시 호출될 함수, 편집된 값 전달
  onCancel: () => void; // Cancel 버튼 클릭 시 호출될 함수
}

const SqlEditModal: React.FC<SqlEditModalProps> = ({
  isOpen,
  title,
  initialValue,
  onOk,
  onCancel,
}) => {
  // 모달 내부에서 관리할 SQL 값 상태
  const [editedSqlValue, setEditedSqlValue] = useState<string>(initialValue);

  // 모달이 열릴 때마다 초기 값으로 상태 업데이트
  useEffect(() => {
    setEditedSqlValue(initialValue);
  }, [initialValue, isOpen]); // initialValue 또는 isOpen이 변경될 때만 실행

  // 입력 필드 값 변경 핸들러
  const handleInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    setEditedSqlValue(event.target.value);
  }, []);

  // OK 버튼 클릭 핸들러: 편집된 값을 부모 컴포넌트로 전달
  const handleOkClick = useCallback(() => {
    onOk(editedSqlValue);
  }, [onOk, editedSqlValue]);

  // Cancel 버튼 클릭 핸들러: 취소 동작 수행
  const handleCancelClick = useCallback(() => {
    onCancel();
  }, [onCancel]);

  // 모달이 열려 있지 않으면 아무것도 렌더링하지 않음
  if (!isOpen) {
    return null;
  }

  // 모달 스타일 (간단한 예시)
  const modalStyle: React.CSSProperties = {
    position: 'fixed',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    backgroundColor: '#fff',
    padding: '20px',
    zIndex: 1000,
    boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
    borderRadius: '8px',
    width: '400px',
    maxWidth: '90%',
    display: 'flex',
    flexDirection: 'column',
  };

  const backdropStyle: React.CSSProperties = {
    position: 'fixed',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    zIndex: 999,
  };

  const inputStyle: React.CSSProperties = {
    width: '100%', // 부모 요소 너비에 맞춤
    marginBottom: '15px',
    padding: '8px',
    boxSizing: 'border-box',
    marginTop: '10px',
  };

  const buttonContainerStyle: React.CSSProperties = {
    display: 'flex',
    justifyContent: 'flex-end',
    gap: '10px', // 버튼 사이 간격
  };

  const buttonStyle: React.CSSProperties = {
     padding: '8px 15px',
     cursor: 'pointer',
     border: '1px solid #ccc',
     borderRadius: '4px',
     backgroundColor: '#eee',
  }

  const okButtonStyle: React.CSSProperties = {
    ...buttonStyle,
    backgroundColor: '#007bff',
    color: 'white',
    border: 'none',
  }


  return (
    <>
      {/* 배경 오버레이 */}
      <div style={backdropStyle} onClick={handleCancelClick}></div>
      {/* 실제 모달 내용 */}
      <div style={modalStyle}>
        <h3 style={{ marginTop: 0, marginBottom: '5px' }}>{title}</h3>
        {/* SQL 값 편집을 위한 텍스트 영역이나 입력 필드 */}
        <input
          type="text"
          value={editedSqlValue}
          onChange={handleInputChange}
          style={inputStyle}
        />
        <div style={buttonContainerStyle}>
          <button onClick={handleOkClick} style={okButtonStyle}>OK</button>
          <button onClick={handleCancelClick} style={buttonStyle}>Cancel</button>
        </div>
      </div>
    </>
  );
};

export default SqlEditModal;

 

GridWithSqlEdit.tsx

// src/components/GridWithSqlEdit.tsx (예시 경로)

import React, { useState, useRef, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { GridApi, GridReadyEvent, ColDef } from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css'; // 사용할 테마 CSS를 선택하세요

// 별도로 정의한 데이터 타입 임포트
import { RowDataType } from '../types/RowDataType'; // 실제 파일 경로에 맞게 수정하세요

// 별도로 정의한 모달 컴포넌트 임포트
import SqlEditModal from './SqlEditModal'; // 실제 파일 경로에 맞게 수정하세요


const GridWithSqlEdit: React.FC = () => {
  // ag-grid API에 접근하기 위한 ref
  const gridApi = useRef<GridApi<RowDataType> | null>(null);

  // 그리드에 표시될 데이터 상태
  const [rowData, setRowData] = useState<RowDataType[]>([
    { id: 1, make: "Toyota", model: "Celica", price: 35000, sql: "SELECT * FROM cars WHERE id = 1" },
    { id: 2, make: "Ford", model: "Mondeo", price: 32000, sql: "SELECT user_name FROM users WHERE status = 'active'" },
    { id: 3, make: "Porsche", model: "Boxster", price: 72000, sql: "SELECT count(*) FROM orders WHERE order_date > '2023-01-01'" },
    { id: 4, make: "BMW", model: "M5", price: 60000, sql: "SELECT product_name, price FROM products WHERE category = 'electronics'" },
    { id: 5, make: "Audi", model: "A4", price: 40000, sql: "SELECT inventory_level FROM warehouse WHERE location = 'A'" },
  ]);

  // SQL 편집 모달 창 열림/닫힘 상태
  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
  // 모달에 전달할 초기 SQL 값 (첫 번째 선택된 행의 SQL 값)
  const [sqlValueForModal, setSqlValueForModal] = useState<string>('');
  // SQL 편집 버튼 활성화 상태 (선택된 행이 1개 이상일 때 활성화)
  const [isEditButtonEnabled, setIsEditButtonEnabled] = useState<boolean>(false);


  // 컬럼 정의
  const columnDefs = useMemo<ColDef[]>(() => [
    {
      field: 'make',
      headerName: '제조사',
      checkboxSelection: true, // 이 컬럼에 체크박스 표시
      headerCheckboxSelection: true, // 헤더에 전체 선택 체크박스 표시
    },
    { field: 'model', headerName: '모델명' },
    { field: 'price', headerName: '가격' },
    { field: 'sql', headerName: 'SQL', editable: false }, // SQL 필드 (그리드 내에서는 직접 편집 방지)
  ], []);

  // 기본 컬럼 속성 정의
  const defaultColDef = useMemo<ColDef>(() => ({
    flex: 1,
    minWidth: 100,
    sortable: true,
    filter: true,
    resizable: true, // 컬럼 크기 조절 가능
  }), []);

  // 그리드 준비 완료 시 호출되는 콜백
  const onGridReady = useCallback((params: GridReadyEvent) => {
    gridApi.current = params.api;
  }, []);

  // 행 선택 상태 변경 시 호출되는 콜백
  const onSelectionChanged = useCallback(() => {
    if (!gridApi.current) return;
    // 선택된 행이 1개 이상인지 확인하여 버튼 활성화 상태 업데이트
    const selectedNodes = gridApi.current.getSelectedNodes();
    setIsEditButtonEnabled(selectedNodes.length > 0);
  }, []);

  // '선택된 SQL 수정' 버튼 클릭 핸들러
  const handleEditSelectedSqlClick = useCallback(() => {
    if (!gridApi.current) return;

    const selectedNodes = gridApi.current.getSelectedNodes();

    // 선택된 행이 하나 이상인 경우에만 모달 열기 로직 실행
    if (selectedNodes.length > 0) {
      // 첫 번째 선택된 행의 SQL 값을 가져와 모달에 전달할 상태에 저장
      const firstSelectedSql = selectedNodes[0]?.data?.sql || '';
      setSqlValueForModal(firstSelectedSql);
      // 모달 열기
      setIsModalOpen(true);
    } else {
      // 선택된 행이 없으면 버튼 비활성화 상태 유지 (onSelectionChanged에서 처리됨)
      console.log("수정할 행을 선택해주세요.");
    }
  }, []); // 의존성 배열 비워둠 - gridApi.current는 변경되지 않음

  // 모달에서 'OK' 버튼 클릭 시 호출될 콜백 (모달로부터 편집된 값 전달받음)
  const handleModalOk = useCallback((editedValue: string) => {
    if (!gridApi.current) return;

    const selectedNodes = gridApi.current.getSelectedNodes();
    const updatedRows: RowDataType[] = [];

    // 모달에서 전달받은 편집된 값으로 선택된 모든 행의 SQL 필드를 업데이트
    selectedNodes.forEach(node => {
      if (node.data) {
        const updatedRow = { ...node.data, sql: editedValue };
        updatedRows.push(updatedRow);
      }
    });

    // applyTransaction을 사용하여 그리드 데이터 업데이트
    gridApi.current.applyTransaction({ update: updatedRows });

    // 모달 닫기
    setIsModalOpen(false);
    // 모달에 전달했던 SQL 값 상태 초기화
    setSqlValueForModal('');
     // 선택 해제
    gridApi.current.deselectAll();
  }, []);

  // 모달에서 'Cancel' 버튼 클릭 시 호출될 콜백
  const handleModalCancel = useCallback(() => {
    // 모달 닫기
    setIsModalOpen(false);
    // 모달에 전달했던 SQL 값 상태 초기화
    setSqlValueForModal('');
     // 선택 해제
    gridApi.current.deselectAll();
  }, []);


  return (
    <div style={{ height: 450, width: '100%', display: 'flex', flexDirection: 'column' }}> {/* 전체 컨테이너 스타일 */}
      {/* SQL 수정 버튼 */}
      <div style={{ marginBottom: '10px' }}>
        <button
          onClick={handleEditSelectedSqlClick}
          disabled={!isEditButtonEnabled} // 선택된 행이 없으면 버튼 비활성화
          style={{ padding: '8px 15px', cursor: isEditButtonEnabled ? 'pointer' : 'not-allowed' }}
        >
          선택된 SQL 수정
        </button>
      </div>

      {/* ag-grid 컨테이너 */}
      <div className="ag-theme-alpine" style={{ flexGrow: 1, width: '100%' }}> {/* 남은 공간을 모두 사용하도록 flexGrow 설정 */}
        <AgGridReact<RowDataType>
          ref={gridApi as any}
          rowData={rowData}
          columnDefs={columnDefs}
          defaultColDef={defaultColDef}
          rowSelection="multiple"
          onGridReady={onGridReady}
          onSelectionChanged={onSelectionChanged} // 선택 상태 변경 감지 (버튼 활성화/비활성화용)
        />
      </div>

      {/* SQL 편집 모달 컴포넌트 */}
      <SqlEditModal
        isOpen={isModalOpen} // 모달 열림 상태 전달
        title="선택된 행의 SQL 값 수정" // 모달 제목
        initialValue={sqlValueForModal} // 모달에 초기 값 전달
        onOk={handleModalOk} // OK 버튼 클릭 시 호출될 함수 전달
        onCancel={handleModalCancel} // Cancel 버튼 클릭 시 호출될 함수 전달
      />
    </div>
  );
};

export default GridWithSqlEdit;

 

React17, Typescript, ag-grid 사용하는 환경에서 ag-grid의 처음에 있는 체크박스를 선택하면 여러개의 경우 0 번쨰 row의 SQL 필드의 값을 가지고 타이틀이 있고 아래 textbox에 SQL 값이 나타나고 해당 SQL 값을 수정하고 OK 하면 선택 된 ag-grid에 값이 입력되고 cancle 버튼을 클릭하면 반영하지 않는 소스 예제 만들어 주세요.

import React, { useState, useRef, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { GridApi, GridReadyEvent, ColDef } from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css'; // 사용할 테마 CSS를 선택하세요

// 행 데이터 타입 정의
interface RowDataType {
  id: number; // 각 행을 고유하게 식별할 ID 추가 (업데이트 시 필요)
  make: string;
  model: string;
  price: number;
  sql: string; // 사용자의 요청에 따른 SQL 필드
}

const GridWithSqlEdit: React.FC = () => {
  // ag-grid API에 접근하기 위한 ref
  const gridApi = useRef<GridApi<RowDataType> | null>(null);

  // 그리드에 표시될 데이터 상태
  const [rowData, setRowData] = useState<RowDataType[]>([
    { id: 1, make: "Toyota", model: "Celica", price: 35000, sql: "SELECT * FROM cars WHERE id = 1" },
    { id: 2, make: "Ford", model: "Mondeo", price: 32000, sql: "SELECT user_name FROM users WHERE status = 'active'" },
    { id: 3, make: "Porsche", model: "Boxster", price: 72000, sql: "SELECT count(*) FROM orders WHERE order_date > '2023-01-01'" },
    { id: 4, make: "BMW", model: "M5", price: 60000, sql: "SELECT product_name, price FROM products WHERE category = 'electronics'" },
    { id: 5, make: "Audi", model: "A4", price: 40000, sql: "SELECT inventory_level FROM warehouse WHERE location = 'A'" },
  ]);

  // SQL 편집 모달 창 열림/닫힘 상태
  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
  // 모달 창에서 편집할 SQL 값 상태
  const [modalSqlValue, setModalSqlValue] = useState<string>('');

  // 컬럼 정의
  const columnDefs = useMemo<ColDef[]>(() => [
    {
      field: 'id', // 고유 ID 필드 (사용자에게는 숨겨도 됩니다)
      headerName: 'ID',
      hide: true // ID는 내부적으로 사용하고 숨김
    },
    {
      field: 'make',
      headerName: '제조사',
      checkboxSelection: true, // 이 컬럼에 체크박스 표시
      headerCheckboxSelection: true, // 헤더에 전체 선택 체크박스 표시
    },
    { field: 'model', headerName: '모델명' },
    { field: 'price', headerName: '가격' },
    { field: 'sql', headerName: 'SQL', editable: false }, // SQL 필드 (그리드 내에서는 직접 편집 방지)
  ], []);

  // 기본 컬럼 속성 정의
  const defaultColDef = useMemo<ColDef>(() => ({
    flex: 1,
    minWidth: 100,
    sortable: true,
    filter: true,
    resizable: true, // 컬럼 크기 조절 가능
  }), []);

  // 그리드 준비 완료 시 호출되는 콜백
  const onGridReady = useCallback((params: GridReadyEvent) => {
    gridApi.current = params.api;
  }, []);

  // 행 선택 상태 변경 시 호출되는 콜백
  const onSelectionChanged = useCallback(() => {
    if (!gridApi.current) return;

    // 현재 선택된 행 노드들을 가져옵니다.
    const selectedNodes = gridApi.current.getSelectedNodes();
    const selectedData = selectedNodes.map(node => node.data).filter((data): data is RowDataType => data !== undefined);

    // 선택된 행이 하나 이상인 경우 모달을 엽니다.
    if (selectedData.length > 0) {
      // 사용자의 요청에 따라 첫 번째 선택된 행의 SQL 값을 가져와 모달에 설정합니다.
      const firstSelectedSql = selectedData[0]?.sql || '';
      setModalSqlValue(firstSelectedSql);
      setIsModalOpen(true);
    } else {
      // 선택된 행이 없으면 모달을 닫습니다.
      setIsModalOpen(false);
      setModalSqlValue('');
    }
  }, []);

  // 모달 창에서 'OK' 버튼 클릭 시 호출되는 함수
  const handleModalOk = useCallback(() => {
    if (!gridApi.current) return;

    // 현재 선택된 행 노드들을 다시 가져옵니다.
    const selectedNodes = gridApi.current.getSelectedNodes();
    const updatedRows: RowDataType[] = [];

    // 모달에서 편집된 SQL 값으로 선택된 모든 행의 SQL 필드를 업데이트합니다.
    selectedNodes.forEach(node => {
      if (node.data) {
        // 기존 행 데이터를 복사하고 SQL 필드만 업데이트
        const updatedRow = { ...node.data, sql: modalSqlValue };
        updatedRows.push(updatedRow);
      }
    });

    // applyTransaction을 사용하여 그리드 데이터를 효율적으로 업데이트합니다.
    gridApi.current.applyTransaction({ update: updatedRows });

    // 모달 창을 닫습니다.
    setIsModalOpen(false);
  }, [modalSqlValue]); // modalSqlValue 상태가 변경될 때마다 함수를 재생성

  // 모달 창에서 'Cancel' 버튼 클릭 시 호출되는 함수
  const handleModalCancel = useCallback(() => {
    // 모달을 닫기만 하고 데이터는 업데이트하지 않습니다.
    setIsModalOpen(false);
  }, []);

  // 모달 입력 필드 값 변경 시 호출되는 함수
  const handleModalInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    setModalSqlValue(event.target.value);
  }, []);

  // 간단한 모달 컴포넌트 (별도의 라이브러리를 사용할 수도 있습니다)
  const Modal = ({ isOpen, title, value, onInputChange, onOk, onCancel }: {
    isOpen: boolean;
    title: string;
    value: string;
    onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
    onOk: () => void;
    onCancel: () => void;
  }) => {
    if (!isOpen) return null; // 모달이 열려 있지 않으면 아무것도 렌더링하지 않음

    // 모달 스타일 (간단한 예시)
    const modalStyle: React.CSSProperties = {
      position: 'fixed',
      top: '50%',
      left: '50%',
      transform: 'translate(-50%, -50%)',
      backgroundColor: '#fff',
      padding: '20px',
      zIndex: 1000, // 그리드 위에 표시되도록 z-index 설정
      boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
      borderRadius: '8px',
      width: '400px', // 모달 너비 지정
      maxWidth: '90%', // 반응형 고려
    };

    // 배경 오버레이 스타일
    const backdropStyle: React.CSSProperties = {
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      backgroundColor: 'rgba(0, 0, 0, 0.5)', // 반투명 배경
      zIndex: 999, // 모달보다는 아래, 다른 콘텐츠보다는 위에 표시
    };

    return (
      <>
        {/* 배경 오버레이 클릭 시 모달 닫기 */}
        <div style={backdropStyle} onClick={onCancel}></div>
        {/* 실제 모달 내용 */}
        <div style={modalStyle}>
          <h3 style={{ marginTop: 0 }}>{title}</h3>
          <input
            type="text"
            value={value}
            onChange={onInputChange}
            style={{ width: 'calc(100% - 16px)', marginBottom: '15px', padding: '8px', boxSizing: 'border-box' }} // 패딩 포함 너비 계산
          />
          <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
            <button onClick={onOk} style={{ marginRight: '10px', padding: '8px 15px', cursor: 'pointer' }}>OK</button>
            <button onClick={onCancel} style={{ padding: '8px 15px', cursor: 'pointer' }}>Cancel</button>
          </div>
        </div>
      </>
    );
  };

  return (
    <div style={{ height: 400, width: '100%' }}> {/* 그리드가 표시될 영역 크기 설정 */}
      {/* ag-grid 컨테이너 */}
      <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
        <AgGridReact<RowDataType>
          ref={gridApi as any} // ag-grid 컴포넌트에 ref 연결
          rowData={rowData} // 그리드 데이터
          columnDefs={columnDefs} // 컬럼 정의
          defaultColDef={defaultColDef} // 기본 컬럼 속성
          rowSelection="multiple" // 다중 행 선택 허용
          onGridReady={onGridReady} // 그리드 준비 완료 시 호출
          onSelectionChanged={onSelectionChanged} // 행 선택 상태 변경 시 호출
        />
      </div>

      {/* SQL 편집 모달 컴포넌트 */}
      <Modal
        isOpen={isModalOpen} // 모달 열림 상태 전달
        title="선택된 행의 SQL 값 수정" // 모달 제목
        value={modalSqlValue} // 모달 입력 필드 값
        onInputChange={handleModalInputChange} // 입력 값 변경 핸들러
        onOk={handleModalOk} // OK 버튼 클릭 핸들러
        onCancel={handleModalCancel} // Cancel 버튼 클릭 핸들러
      />
    </div>
  );
};

export default GridWithSqlEdit;

 

 

함수형 컴포넌트 (Function Components) vs 클래스형 컴포넌트 (Class Components)

1. 함수형 컴포넌트 (Function Components)

마치 레고 블록처럼, 함수형 컴포넌트는 JavaScript의 평범한 함수로 정의돼요. props라는 입력을 받아서 React 엘리먼트를 반환하는 단순한 구조를 가지고 있죠.

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 또는 화살표 함수를 사용하기도 해요.
const Welcome = (props) => {
  return <h1>Hello, {props.name}</h1>;
};

 

특징:

  • 간결성: 클래스형 컴포넌트에 비해 코드가 훨씬 간결하고 이해하기 쉬워요. 마치 짧은 메시지처럼 핵심 내용만 담고 있죠.
  • 순수 함수: 원칙적으로 props가 주어지면 항상 동일한 결과를 반환하는 순수 함수로 작성될 수 있어요. 이는 예측 가능성을 높이고 테스트를 용이하게 만들죠. 마치 정해진 재료로 항상 똑같은 맛을 내는 요리사와 같아요.
  • Hook의 등장: React 16.8 버전부터 Hook이라는 기능이 도입되면서 함수형 컴포넌트에서도 상태 관리, 생명주기 기능 등 클래스형 컴포넌트에서만 가능했던 다양한 기능을 사용할 수 있게 되었어요. 마치 레고 블록에 특별한 기능을 가진 부품들이 추가된 것과 같아요.

2. 클래스형 컴포넌트 (Class Components)

클래스형 컴포넌트는 ES6의 class 문법을 사용하여 정의되며, React.Component를 상속받아요. 마치 좀 더 복잡한 건축 설계도면처럼, 상태 관리, 생명주기 메서드 등 다양한 기능을 내장하고 있죠.

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

 

 

특징:

  • 상태 관리 (State): this.state를 통해 컴포넌트 내부에서 변화하는 데이터를 관리할 수 있어요. 마치 건물의 냉난방 시스템처럼, 컴포넌트의 내부 상태를 조절할 수 있죠.
  • 생명주기 메서드: 컴포넌트가 생성될 때, 업데이트될 때, 소멸될 때 등 특정 시점에 실행되는 메서드들을 제공해요. 마치 건물의 건설, 유지 보수, 철거 단계에 맞춰 수행되는 작업들과 같아요. (예: componentDidMount, componentDidUpdate, componentWillUnmount)
  • this 바인딩: 메서드 내에서 this 키워드를 사용할 때 주의가 필요하며, 종종 명시적인 바인딩 (.bind(this))이 필요할 수 있어요. 마치 복잡한 기계를 다룰 때 각 부품의 역할을 정확히 이해해야 하는 것과 비슷해요.

주요 차이점 요약:

특징함수형 컴포넌트클래스형 컴포넌트
정의 JavaScript 함수 ES6 class (React.Component 상속)
상태 관리 Hook (useState) 사용 this.state 사용
생명주기 Hook (useEffect) 사용 생명주기 메서드 (예: componentDidMount) 사용
코드 양 일반적으로 더 간결함 일반적으로 더 많은 코드 필요

 

 

 

반응형