roomescape-refactored/frontend/src/pages/admin/AdminThemeEditPage.tsx
pricelees bdc99c7883 [#37] 테마 스키마 재정의 (#38)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

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

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- 가격, 시간 등 테마를 정의하는데 필요하다고 느껴지는 필드 추가
- JPA Auditing으로 감사 정보 확인 기능 추가
- 프론트엔드 페이지 디자인 변경 및 새로운 API 반영

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
6db81feb9b 을 바탕으로 향후 다른 모든 기능의 테스트를 통합 테스트로 전환할 예정. 지금은 불필요한 테스트가 너무 많다고 느껴짐.

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->
- FInder / Writer / Validator 구조를 수정할 필요가 있음. 복잡하고 가독성이 낮은 로직만 별도로 빼는 것이 더 효율적이라고 판단됨.

Reviewed-on: #38
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-09-03 02:03:37 +00:00

267 lines
13 KiB
TypeScript

import { isLoginRequiredError } from '@_api/apiClient';
import {
createThemeV2,
deleteTheme,
fetchAdminThemeDetail,
updateTheme
} from '@_api/theme/themeAPI';
import { Difficulty, type ThemeCreateRequestV2, type ThemeUpdateRequest, type ThemeV2 } 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<ThemeV2 | ThemeCreateRequestV2 | null>(null);
const [originalTheme, setOriginalTheme] = useState<ThemeV2 | ThemeCreateRequestV2 | 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: ThemeCreateRequestV2 = {
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: ThemeV2 = {
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 createThemeV2(theme as ThemeCreateRequestV2);
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"> ()</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;