roomescape-refactored/frontend/src/pages/admin/AdminSchedulePage.tsx

316 lines
15 KiB
TypeScript

import { isLoginRequiredError } from '@_api/apiClient';
import { createSchedule, deleteSchedule, findScheduleById, findSchedules, updateSchedule } from '@_api/schedule/scheduleAPI';
import { ScheduleStatus, type ScheduleDetailRetrieveResponse, type ScheduleRetrieveResponse } from '@_api/schedule/scheduleTypes';
import { fetchAdminThemes } from '@_api/theme/themeAPI';
import type { AdminThemeSummaryRetrieveResponse } from '@_api/theme/themeTypes';
import '@_css/admin-schedule-page.css';
import React, { Fragment, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const getScheduleStatusText = (status: ScheduleStatus): string => {
switch (status) {
case ScheduleStatus.AVAILABLE:
return '예약 가능';
case ScheduleStatus.PENDING:
return '예약 진행 중';
case ScheduleStatus.RESERVED:
return '예약 완료';
case ScheduleStatus.BLOCKED:
return '예약 불가';
default:
return status;
}
};
const AdminSchedulePage: React.FC = () => {
const [schedules, setSchedules] = useState<ScheduleRetrieveResponse[]>([]);
const [themes, setThemes] = useState<AdminThemeSummaryRetrieveResponse[]>([]);
const [selectedThemeId, setSelectedThemeId] = useState<string>('');
const [selectedDate, setSelectedDate] = useState<string>(new Date().toLocaleDateString('en-CA'));
const [isAdding, setIsAdding] = useState(false);
const [newScheduleTime, setNewScheduleTime] = useState('');
const [expandedScheduleId, setExpandedScheduleId] = useState<string | null>(null);
const [detailedSchedules, setDetailedSchedules] = useState<{ [key: string]: ScheduleDetailRetrieveResponse }>({});
const [isLoadingDetails, setIsLoadingDetails] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState(false);
const [editingSchedule, setEditingSchedule] = useState<ScheduleDetailRetrieveResponse | null>(null);
const navigate = useNavigate();
const location = useLocation();
const handleError = (err: any) => {
if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } });
} else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message);
console.error(err);
}
};
useEffect(() => {
fetchAdminThemes()
.then(res => {
setThemes(res.themes);
if (res.themes.length > 0) {
setSelectedThemeId(String(res.themes[0].id));
}
})
.catch(handleError);
}, []);
const fetchSchedules = () => {
if (selectedDate && selectedThemeId) {
findSchedules(selectedDate, selectedThemeId)
.then(res => setSchedules(res.schedules))
.catch(err => {
setSchedules([]);
if (err.response?.status !== 404) {
handleError(err);
}
});
}
}
useEffect(() => {
fetchSchedules();
}, [selectedDate, selectedThemeId]);
const handleAddSchedule = async () => {
if (!newScheduleTime) {
alert('시간을 입력해주세요.');
return;
}
if (!/\d{2}:\d{2}/.test(newScheduleTime)) {
alert('시간 형식이 올바르지 않습니다. HH:MM 형식으로 입력해주세요.');
return;
}
try {
await createSchedule({
date: selectedDate,
themeId: selectedThemeId,
time: newScheduleTime,
});
fetchSchedules();
setIsAdding(false);
setNewScheduleTime('');
} catch (error) {
handleError(error);
}
};
const handleDeleteSchedule = async (scheduleId: string) => {
if (window.confirm('정말 이 일정을 삭제하시겠습니까?')) {
try {
await deleteSchedule(scheduleId);
setSchedules(schedules.filter(s => s.id !== scheduleId));
setExpandedScheduleId(null); // Close the details view after deletion
} catch (error) {
handleError(error);
}
}
};
const handleToggleDetails = async (scheduleId: string) => {
const isAlreadyExpanded = expandedScheduleId === scheduleId;
setIsEditing(false); // Reset editing state whenever toggling
if (isAlreadyExpanded) {
setExpandedScheduleId(null);
} else {
setExpandedScheduleId(scheduleId);
if (!detailedSchedules[scheduleId]) {
setIsLoadingDetails(true);
try {
const details = await findScheduleById(scheduleId);
setDetailedSchedules(prev => ({ ...prev, [scheduleId]: details }));
} catch (error) {
handleError(error);
} finally {
setIsLoadingDetails(false);
}
}
}
};
const handleEditClick = () => {
if (expandedScheduleId && detailedSchedules[expandedScheduleId]) {
setEditingSchedule({ ...detailedSchedules[expandedScheduleId] });
setIsEditing(true);
}
};
const handleCancelEdit = () => {
setIsEditing(false);
setEditingSchedule(null);
};
const handleEditChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
if (editingSchedule) {
setEditingSchedule({ ...editingSchedule, [name]: value });
}
};
const handleSave = async () => {
if (!editingSchedule) return;
try {
await updateSchedule(editingSchedule.id, {
time: editingSchedule.time,
status: editingSchedule.status,
});
// Refresh data
const details = await findScheduleById(editingSchedule.id);
setDetailedSchedules(prev => ({ ...prev, [editingSchedule.id]: details }));
setSchedules(schedules.map(s => s.id === editingSchedule.id ? { ...s, time: details.time, status: details.status } : s));
alert('일정이 성공적으로 업데이트되었습니다.');
setIsEditing(false);
} catch (error) {
handleError(error);
}
};
return (
<div className="admin-schedule-container">
<h2 className="page-title"> </h2>
<div className="schedule-controls">
<div className="form-group">
<label className="form-label" htmlFor="date-filter"></label>
<input
id="date-filter"
type="date"
className="form-input"
value={selectedDate}
onChange={e => setSelectedDate(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label" htmlFor="theme-filter"></label>
<select
id="theme-filter"
className="form-select"
value={selectedThemeId}
onChange={e => setSelectedThemeId(e.target.value)}
>
{themes.map(theme => (
<option key={theme.id} value={theme.id}>{theme.name}</option>
))}
</select>
</div>
</div>
<div className="section-card">
<div className="table-header">
<button className="btn btn-primary" onClick={() => setIsAdding(true)}> </button>
</div>
<div className="table-container">
<table>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{schedules.map(schedule => (
<Fragment key={schedule.id}>
<tr>
<td>{schedule.time}</td>
<td>{getScheduleStatusText(schedule.status)}</td>
<td className="action-buttons">
<button
className="btn btn-secondary"
onClick={() => handleToggleDetails(schedule.id)}
>
{expandedScheduleId === schedule.id ? '닫기' : '상세'}
</button>
</td>
</tr>
{expandedScheduleId === schedule.id && (
<tr className="schedule-details-row">
<td colSpan={3}>
{isLoadingDetails ? (
<p> ...</p>
) : detailedSchedules[schedule.id] ? (
<div className="details-form-container">
<div className="audit-info">
<h4 className="audit-title"> </h4>
<div className="audit-body">
<p><strong>:</strong> {new Date(detailedSchedules[schedule.id].createdAt).toLocaleString()}</p>
<p><strong>:</strong> {new Date(detailedSchedules[schedule.id].updatedAt).toLocaleString()}</p>
<p><strong>:</strong> {detailedSchedules[schedule.id].createdBy}</p>
<p><strong>:</strong> {detailedSchedules[schedule.id].updatedBy}</p>
</div>
</div>
{isEditing && editingSchedule ? (
// --- EDIT MODE ---
<div className="form-card">
<div className="form-section">
<div className="form-row">
<div className="form-group">
<label className="form-label"></label>
<input type="time" name="time" className="form-input" value={editingSchedule.time} onChange={handleEditChange} />
</div>
<div className="form-group">
<label className="form-label"></label>
<select name="status" className="form-select" value={editingSchedule.status} onChange={handleEditChange}>
{Object.values(ScheduleStatus).map(s => <option key={s} value={s}>{getScheduleStatusText(s)}</option>)}
</select>
</div>
</div>
</div>
<div className="button-group">
<button type="button" className="btn btn-secondary" onClick={handleCancelEdit}></button>
<button type="button" className="btn btn-primary" onClick={handleSave}></button>
</div>
</div>
) : (
// --- VIEW MODE ---
<div className="button-group view-mode-buttons">
<button type="button" className="btn btn-danger" onClick={() => handleDeleteSchedule(schedule.id)}></button>
<button type="button" className="btn btn-primary" onClick={handleEditClick}></button>
</div>
)}
</div>
) : (
<p> .</p>
)}
</td>
</tr>
)}
</Fragment>
))}
{isAdding && (
<tr className="editing-row">
<td>
<input
type="time"
className="form-input"
value={newScheduleTime}
onChange={e => setNewScheduleTime(e.target.value)}
/>
</td>
<td></td>
<td className="action-buttons">
<button className="btn btn-primary" onClick={handleAddSchedule}></button>
<button className="btn btn-secondary" onClick={() => setIsAdding(false)}></button>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default AdminSchedulePage;