기타 보관함/개발자정보

React 17.0.2 모달 창 띄우기(함수형)

오아름 샘 2025. 5. 10. 08:34
반응형

App.tsx

import React, { useState, useEffect } 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;
}

const App: React.FC = () => {
  // 상태 관리
  const [rowData, setRowData] = useState<MainRowData[]>([]);
  const [selectedRow, setSelectedRow] = useState<MainRowData | null>(null);
  const [isViewSqlModalOpen, setIsViewSqlModalOpen] = useState<boolean>(false);
  const [isAddModalOpen, setIsAddModalOpen] = useState<boolean>(false);
  
  // 초기 데이터 로드 (실제 환경에서는 API 호출로 대체)
  useEffect(() => {
    // 샘플 데이터
    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' },
    ];
    
    setRowData(sampleData);
  }, []);

  // 메인 그리드 컬럼 정의
  const 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 }
  ];

  // 행 선택 이벤트 핸들러
  const onSelectionChanged = (event: any) => {
    const selectedRows = event.api.getSelectedRows();
    if (selectedRows.length > 0) {
      setSelectedRow(selectedRows[0]);
    } else {
      setSelectedRow(null);
    }
  };

  // View SQL 버튼 클릭 핸들러
  const handleViewSqlClick = () => {
    if (selectedRow) {
      setIsViewSqlModalOpen(true);
    } else {
      alert('먼저 항목을 선택해주세요.');
    }
  };

  // Add 버튼 클릭 핸들러
  const handleAddClick = () => {
    setIsAddModalOpen(true);
  };

  // SQL 업데이트 핸들러 (ViewSqlModal에서 호출)
  const handleSqlUpdate = (updatedSql: string) => {
    if (selectedRow) {
      const updatedRowData = rowData.map(row => 
        row.id === selectedRow.id ? { ...row, sql: updatedSql } : row
      );
      setRowData(updatedRowData);
      setSelectedRow({ ...selectedRow, sql: updatedSql });
      setIsViewSqlModalOpen(false);
    }
  };

  // 새 파트너 추가 핸들러 (AddPartnerModal에서 호출)
  const handleAddPartners = (newPartners: MainRowData[]) => {
    // 새 ID 생성을 위한 로직
    const maxId = Math.max(...rowData.map(row => row.id), 0);
    const partnersWithIds = newPartners.map((partner, index) => ({
      ...partner,
      id: maxId + index + 1
    }));
    
    setRowData([...rowData, ...partnersWithIds]);
    setIsAddModalOpen(false);
  };

  return (
    <div className="app-container">
      <h1>파트너사 관리</h1>
      
      <div className="button-container">
        <button onClick={handleViewSqlClick} disabled={!selectedRow}>View SQL</button>
        <button onClick={handleAddClick}>Add</button>
      </div>
      
      <div className="ag-theme-alpine" style={{ height: 500, width: '100%' }}>
        <AgGridReact
          rowData={rowData}
          columnDefs={columnDefs}
          rowSelection="single"
          onSelectionChanged={onSelectionChanged}
          pagination={true}
          paginationPageSize={10}
        />
      </div>
      
      {isViewSqlModalOpen && selectedRow && (
        <ViewSqlModal 
          isOpen={isViewSqlModalOpen}
          onClose={() => setIsViewSqlModalOpen(false)}
          sql={selectedRow.sql}
          onSave={handleSqlUpdate}
        />
      )}
      
      {isAddModalOpen && (
        <AddPartnerModal 
          isOpen={isAddModalOpen}
          onClose={() => setIsAddModalOpen(false)}
          onAdd={handleAddPartners}
        />
      )}
    </div>
  );
};

export default App;

 

ViewSqlModal.tsx

import React, { useState } from 'react';
import './Modal.css';

interface ViewSqlModalProps {
  isOpen: boolean;
  onClose: () => void;
  sql: string;
  onSave: (updatedSql: string) => void;
}

export const ViewSqlModal: React.FC<ViewSqlModalProps> = ({ isOpen, onClose, sql, onSave }) => {
  const [sqlText, setSqlText] = useState<string>(sql);
  const [checkResult, setCheckResult] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  if (!isOpen) return null;

  // SQL 텍스트 변경 핸들러
  const handleSqlChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setSqlText(e.target.value);
    // 변경 시 체크 결과 초기화
    setCheckResult(null);
  };

  // SQL 유효성 체크 함수
  const handleCheckSql = async () => {
    try {
      setIsLoading(true);
      setCheckResult(null);
      
      // 실제 환경에서는 아래 코드를 서버 API 호출로 대체
      // const response = await fetch('/api/check-sql', {
      //   method: 'POST',
      //   headers: { 'Content-Type': 'application/json' },
      //   body: JSON.stringify({ sql: sqlText })
      // });
      // const data = await response.json();
      
      // 테스트를 위한 목업 응답
      await new Promise(resolve => setTimeout(resolve, 1000)); // 서버 호출 시뮬레이션
      
      // 임시 검증 로직 (실제로는 서버에서 처리)
      const isValid = sqlText.trim().toUpperCase().startsWith('SELECT');
      
      if (isValid) {
        setCheckResult("SQL 문법이 유효합니다.");
      } else {
        setCheckResult("SQL 문법 오류: SELECT 문으로 시작해야 합니다.");
      }
    } catch (error) {
      setCheckResult("SQL 검증 중 오류가 발생했습니다.");
      console.error("SQL 검증 오류:", error);
    } finally {
      setIsLoading(false);
    }
  };

  // OK 버튼 핸들러 - 변경된 SQL을 저장
  const handleOk = () => {
    onSave(sqlText);
  };

  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={handleSqlChange}
            rows={10}
            className="sql-textarea"
          />
          
          {checkResult && (
            <div className={`check-result ${checkResult.includes('오류') ? 'error' : 'success'}`}>
              {checkResult}
            </div>
          )}
        </div>
        <div className="modal-footer">
          <button 
            onClick={handleCheckSql} 
            disabled={isLoading}
            className="btn-check"
          >
            {isLoading ? '검증 중...' : 'Check SQL'}
          </button>
          <button onClick={handleOk} className="btn-ok">OK</button>
          <button onClick={onClose} className="btn-close">Close</button>
        </div>
      </div>
    </div>
  );
};

 

AddPartnerModal.tsx

import React, { useState, useEffect } 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 SearchForm {
  corporateCode: string;
  partnerId: string;
  partnerName: string;
}

export const AddPartnerModal: React.FC<AddPartnerModalProps> = ({ isOpen, onClose, onAdd }) => {
  // react-hook-form 설정
  const { register, handleSubmit, getValues } = useForm<SearchForm>({
    defaultValues: {
      corporateCode: '',
      partnerId: '',
      partnerName: ''
    }
  });

  // 상태 관리
  const [rowData, setRowData] = useState<Partner[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [selectedRows, setSelectedRows] = useState<Partner[]>([]);

  // 컬럼 정의
  const 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 }
  ];

  if (!isOpen) return null;

  // 조회 버튼 핸들러
  const onSearch = async (data: SearchForm) => {
    try {
      setIsLoading(true);
      
      // 실제 환경에서는 아래 코드를 서버 API 호출로 대체
      // const response = await fetch('/api/search-partners', {
      //   method: 'POST',
      //   headers: { 'Content-Type': 'application/json' },
      //   body: JSON.stringify(data)
      // });
      // 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 (
          (!data.corporateCode || item.corporateCode.includes(data.corporateCode)) &&
          (!data.partnerId || item.partnerId.includes(data.partnerId)) &&
          (!data.partnerName || item.partnerName.includes(data.partnerName))
        );
      });
      
      setRowData(filteredData);
    } catch (error) {
      console.error("파트너 검색 오류:", error);
      alert("파트너 검색 중 오류가 발생했습니다.");
    } finally {
      setIsLoading(false);
    }
  };

  // 행 선택 변경 이벤트 핸들러
  const onSelectionChanged = (event: any) => {
    const selectedNodes = event.api.getSelectedNodes();
    const selectedData = selectedNodes.map((node: any) => node.data);
    setSelectedRows(selectedData);
  };

  // OK 버튼 핸들러
  const handleOk = () => {
    if (selectedRows.length === 0) {
      alert('추가할 항목을 선택해주세요.');
      return;
    }
    onAdd(selectedRows);
  };

  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={handleSubmit(onSearch)} className="search-form">
            <div className="form-group">
              <label>법인</label>
              <input type="text" {...register('corporateCode')} />
            </div>
            
            <div className="form-group">
              <label>파트너사 ID</label>
              <input type="text" {...register('partnerId')} />
            </div>
            
            <div className="form-group">
              <label>파트너 명</label>
              <input type="text" {...register('partnerName')} />
            </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={columnDefs}
              rowSelection="multiple"
              onSelectionChanged={onSelectionChanged}
              pagination={true}
              paginationPageSize={5}
            />
          </div>
        </div>
        
        <div className="modal-footer">
          <button onClick={handleOk} className="btn-ok">OK</button>
          <button onClick={onClose} className="btn-close">Close</button>
        </div>
      </div>
    </div>
  );
};

 

Modal.css

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 5px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  width: 80%;
  max-width: 800px;
  display: flex;
  flex-direction: column;
  max-height: 90vh;
}

.add-partner-modal {
  width: 90%;
  max-width: 1000px;
}

.modal-header {
  padding: 15px 20px;
  border-bottom: 1px solid #e0e0e0;
}

.modal-header h2 {
  margin: 0;
  font-size: 1.5rem;
}

.modal-body {
  padding: 20px;
  overflow-y: auto;
  flex-grow: 1;
}

.modal-footer {
  padding: 15px 20px;
  border-top: 1px solid #e0e0e0;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.sql-textarea {
  width: 100%;
  resize: vertical;
  padding: 10px;
  font-family: monospace;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.check-result {
  margin-top: 15px;
  padding: 10px;
  border-radius: 4px;
}

.check-result.success {
  background-color: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.check-result.error {
  background-color: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}

button {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  border: 1px solid #ccc;
  background-color: #f8f9fa;
  font-size: 14px;
}

button:hover {
  background-color: #e9ecef;
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-ok {
  background-color: #007bff;
  color: white;
  border-color: #007bff;
}

.btn-ok:hover {
  background-color: #0069d9;
  border-color: #0062cc;
}

.btn-check {
  background-color: #6c757d;
  color: white;
  border-color: #6c757d;
}

.btn-check:hover {
  background-color: #5a6268;
  border-color: #545b62;
}

.search-form {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 15px;
  margin-bottom: 20px;
}

.form-group {
  display: flex;
  flex-direction: column;
}

.form-group label {
  margin-bottom: 5px;
  font-weight: 500;
}

.form-group input {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.btn-search {
  height: 36px;
  margin-top: 22px;
  background-color: #28a745;
  color: white;
  border-color: #28a745;
}

.btn-search:hover {
  background-color: #218838;
  border-color: #1e7e34;
}

 

 

App.css

.app-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}

h1 {
  color: #333;
  margin-bottom: 20px;
}

.button-container {
  margin-bottom: 20px;
  display: flex;
  gap: 10px;
}

.button-container button {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  border: 1px solid #ccc;
  background-color: #f8f9fa;
  font-size: 14px;
}

.button-container button:hover {
  background-color: #e9ecef;
}

.button-container button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* Grid 컨테이너 스타일 */
.ag-theme-alpine {
  --ag-header-height: 40px;
  --ag-header-background-color: #f5f7fa;
  --ag-header-foreground-color: #333;
  --ag-header-border-color: #dde2eb;
  --ag-row-border-color: #f0f0f0;
  --ag-odd-row-background-color: #fafafa;
}

/* 그리드 내부 셀 스타일 */
.ag-theme-alpine .ag-cell {
  padding: 8px 15px;
}

/* 그리드 헤더 스타일 */
.ag-theme-alpine .ag-header-cell {
  font-weight: 600;
}

/* 선택된 행 스타일 */
.ag-theme-alpine .ag-row-selected {
  background-color: rgba(0, 123, 255, 0.1);
}
 

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')
);

 

index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #f5f5f5;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

* {
  box-sizing: border-box;
}

 

 

 

 

 

 

 

반응형