roomescape-refactored/frontend/src/pages/admin/AdminThemeEditPage.tsx
pricelees 5658f6c31f [#34] 회원 / 인증 도메인 재정의 (#43)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #34

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- 회원 테이블과 관리자 테이블 분리 및 관리자 계정의 예약 기능 제거
- API 인증을 모두(Public) / 회원 전용(UserOnly) / 관리자 전용(AdminOnly) / 회원 + 관리자(Authenticated) 로 세분화해서 구분
- 관리자의 경우 API 접근 권한 세분화 등 인증 로직 개선
- 전체 리팩터링이 완료되어 레거시 코드 제거

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
<img width="750" alt="스크린샷 2025-09-13 19.11.44.png" src="attachments/11e1a79c-9723-4843-839d-be6158d94130">

- 추가 & 변경된 모든 API에 대한 통합 테스트 진행

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #43
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-09-13 10:13:45 +00:00

267 lines
13 KiB
TypeScript

import {isLoginRequiredError} from '@_api/apiClient';
import {createTheme, deleteTheme, fetchAdminThemeDetail, updateTheme} from '@_api/theme/themeAPI';
import {
type AdminThemeDetailResponse,
Difficulty,
type ThemeCreateRequest,
type ThemeUpdateRequest
} from '@_api/theme/themeTypes';
import React, {useEffect, useState} from 'react';
import {useLocation, useNavigate, useParams} from 'react-router-dom';
import '@_css/admin-theme-edit-page.css';
const AdminThemeEditPage: React.FC = () => {
const { themeId } = useParams<{ themeId: string }>();
const navigate = useNavigate();
const location = useLocation();
const isNew = themeId === 'new';
const [theme, setTheme] = useState<AdminThemeDetailResponse | ThemeCreateRequest | null>(null);
const [originalTheme, setOriginalTheme] = useState<AdminThemeDetailResponse | ThemeCreateRequest | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isEditing, setIsEditing] = useState(isNew);
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(() => {
if (isNew) {
const newTheme: ThemeCreateRequest = {
name: '',
description: '',
thumbnailUrl: '',
difficulty: Difficulty.NORMAL,
price: 0,
minParticipants: 2,
maxParticipants: 4,
availableMinutes: 60,
expectedMinutesFrom: 50,
expectedMinutesTo: 70,
isOpen: true,
};
setTheme(newTheme);
setOriginalTheme(newTheme);
setIsLoading(false);
} else if (themeId) {
fetchAdminThemeDetail(themeId)
.then(data => {
// Map AdminThemeDetailRetrieveResponse to ThemeV2
const fetchedTheme: AdminThemeDetailResponse = {
id: data.id,
name: data.name,
description: data.description,
thumbnailUrl: data.thumbnailUrl,
difficulty: data.difficulty,
price: data.price,
minParticipants: data.minParticipants,
maxParticipants: data.maxParticipants,
availableMinutes: data.availableMinutes,
expectedMinutesFrom: data.expectedMinutesFrom,
expectedMinutesTo: data.expectedMinutesTo,
isOpen: data.isOpen,
createDate: data.createdAt, // Map createdAt to createDate
updatedDate: data.updatedAt, // Map updatedAt to updatedDate
createdBy: data.createdBy,
updatedBy: data.updatedBy,
};
setTheme(fetchedTheme);
setOriginalTheme(fetchedTheme);
})
.catch(handleError)
.finally(() => setIsLoading(false));
}
}, [themeId, isNew, navigate]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
let processedValue: string | number | boolean = value;
if (name === 'isOpen') {
processedValue = value === 'true';
} else if (type === 'checkbox') {
processedValue = (e.target as HTMLInputElement).checked;
} else if (type === 'number') {
processedValue = value === '' ? '' : Number(value);
}
setTheme(prev => prev ? { ...prev, [name]: processedValue } : null);
};
const handleCancelEdit = () => {
if (!isNew) {
setTheme(originalTheme);
setIsEditing(false);
} else {
navigate('/admin/theme');
}
};
const handleSubmit = async (e: React.FormEvent) => {
console.log('handleSubmit called');
e.preventDefault();
if (!theme) return;
try {
if (isNew) {
await createTheme(theme as ThemeCreateRequest);
alert('테마가 성공적으로 생성되었습니다.');
navigate(`/admin/theme`);
} else {
if (!themeId) {
throw new Error('themeId is undefined');
}
await updateTheme(themeId, theme as ThemeUpdateRequest);
alert('테마가 성공적으로 업데이트되었습니다.');
setOriginalTheme(theme);
setIsEditing(false);
navigate(`/admin/theme`);
}
} catch (error) {
handleError(error);
}
};
const handleDelete = async () => {
if (isNew || !themeId) return;
if (window.confirm('정말로 이 테마를 삭제하시겠습니까?')) {
try {
await deleteTheme(themeId);
alert('테마가 삭제되었습니다.');
navigate('/admin/theme');
} catch (error) {
handleError(error);
}
}
};
if (isLoading) {
return <div className="admin-theme-edit-container"><p> ...</p></div>;
}
if (!theme) {
return <div className="admin-theme-edit-container"><p> .</p></div>;
}
return (
<div className="admin-theme-edit-container">
<div className="centered-layout">
<header className="page-header">
<h2 className="page-title">{isNew ? '새 테마 추가' : '테마 정보 수정'}</h2>
</header>
<form onSubmit={handleSubmit} className="form-card">
<div className="form-section">
<div className="form-group full-width">
<label className="form-label" htmlFor="name"> </label>
<input id="name" name="name" type="text" className="form-input" value={theme.name} onChange={handleChange} required disabled={!isEditing} />
</div>
<div className="form-group full-width">
<label className="form-label" htmlFor="description"></label>
<textarea id="description" name="description" className="form-textarea" value={theme.description} onChange={handleChange} required disabled={!isEditing} />
</div>
<div className="form-group full-width">
<label className="form-label" htmlFor="thumbnailUrl"> URL</label>
<input id="thumbnailUrl" name="thumbnailUrl" type="text" className="form-input" value={theme.thumbnailUrl} onChange={handleChange} required disabled={!isEditing} />
</div>
</div>
<div className="form-section">
<div className="form-row">
<div className="form-group">
<label className="form-label" htmlFor="difficulty"></label>
<select id="difficulty" name="difficulty" className="form-select" value={theme.difficulty} onChange={handleChange} disabled={!isEditing}>
{Object.values(Difficulty).map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div className="form-group">
<label className="form-label" htmlFor="isOpen"> </label>
<select id="isOpen" name="isOpen" className="form-select" value={String(theme.isOpen)} onChange={handleChange} disabled={!isEditing}>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="form-label" htmlFor="price">1 ()</label>
<input id="price" name="price" type="number" className="form-input" value={theme.price} onChange={handleChange} required disabled={!isEditing} />
</div>
<div className="form-group">
<label className="form-label" htmlFor="availableMinutes"> ()</label>
<input id="availableMinutes" name="availableMinutes" type="number" className="form-input" value={theme.availableMinutes} onChange={handleChange} required disabled={!isEditing} />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="form-label" htmlFor="expectedMinutesFrom"> ()</label>
<input id="expectedMinutesFrom" name="expectedMinutesFrom" type="number" className="form-input" value={theme.expectedMinutesFrom} onChange={handleChange} required disabled={!isEditing} />
</div>
<div className="form-group">
<label className="form-label" htmlFor="expectedMinutesTo"> ()</label>
<input id="expectedMinutesTo" name="expectedMinutesTo" type="number" className="form-input" value={theme.expectedMinutesTo} onChange={handleChange} required disabled={!isEditing} />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="form-label" htmlFor="minParticipants"> ()</label>
<input id="minParticipants" name="minParticipants" type="number" className="form-input" value={theme.minParticipants} onChange={handleChange} required disabled={!isEditing} />
</div>
<div className="form-group">
<label className="form-label" htmlFor="maxParticipants"> ()</label>
<input id="maxParticipants" name="maxParticipants" type="number" className="form-input" value={theme.maxParticipants} onChange={handleChange} required disabled={!isEditing} />
</div>
</div>
</div>
<div className="button-group">
{isEditing ? (
<div className="main-actions">
<button type="button" className="btn btn-secondary" onClick={handleCancelEdit}></button>
<button type="submit" className="btn btn-primary"></button>
</div>
) : (
<div className="main-actions">
<button type="button" className="btn btn-secondary" onClick={() => navigate('/admin/theme')}></button>
<button type="button" className="btn btn-primary" onClick={(e) => { e.preventDefault(); console.log('setIsEditing(true) called'); setIsEditing(true); }}></button>
</div>
)}
</div>
</form>
{!isNew && 'id' in theme && (
<div className="audit-info">
<h4 className="audit-title"> </h4>
<div className="audit-body">
<p><strong>:</strong> {new Date(theme.createDate).toLocaleString()}</p>
<p><strong>:</strong> {new Date(theme.updatedDate).toLocaleString()}</p>
<p><strong>:</strong> {theme.createdBy}</p>
<p><strong>:</strong> {theme.updatedBy}</p>
</div>
</div>
)}
{!isNew && !isEditing && (
<div className="delete-section">
<button className="btn-delete-text" onClick={handleDelete}> .</button>
</div>
)}
</div>
</div>
);
};
export default AdminThemeEditPage;