generated from pricelees/issue-pr-template
316 lines
15 KiB
TypeScript
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;
|