기타 보관함/개발자정보
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;
}
반응형