반응형
6. 컴포넌트 분석 (3): AdminStudyEdit.jsx
이 컴포넌트는 AdminStudyCreate의 확장판으로, 기존 교육 콘텐츠를 수정하는 기능을 담당합니다. 새로운 데이터를 입력받는 것뿐만 아니라, 기존 데이터를 불러와서 보여주고, 개별 항목을 수정, 추가, 삭제하는 훨씬 복잡한 로직을 포함하고 있습니다.
6.1. 코드 전문 (주석 포함)
// 필요한 훅과 API, Redux 액션들을 모두 가져옵니다.
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"
import { genreApi, studyApi, examApi, audioApi } from "../api/adminApi";
import { setGenres } from "../store/adminSlice";
import { useEffect, useState } from "react";
export default function AdminStudyEdit(props) {
// [*] 훅 초기화
const navigate = useNavigate();
const dispatch = useDispatch();
// `useParams` 훅을 사용하여 URL의 파라미터 값을 가져옵니다.
// 예를 들어, URL이 /admin/study/edit/12 이면, { studyNo: '12' } 객체가 반환됩니다.
const { studyNo } = useParams();
const genres = useSelector(state => state.admin.genres);
// [*] 컴포넌트 지역 상태(Local State) 정의
// [상태 1] 주제 데이터: 생성 페이지와 달리, 수정할 studyNo를 포함합니다.
const [studyData, setStudyData] = useState({
studyNo: parseInt(studyNo), // URL에서 받은 studyNo를 정수형으로 저장
themeKo: "", /*...*/, genreNo: ""
});
// [상태 2] 예문 데이터: 서버에서 불러온 기존 예문 목록을 저장할 배열
const [examList, setExamList] = useState([]);
// [상태 3] 로딩 상태: 데이터 로딩 중에 사용자에게 피드백을 주기 위함
const [loading, setLoading] = useState(true);
// [*] 컴포넌트 마운트 시, 수정할 데이터를 서버에서 모두 불러옵니다.
useEffect(() => {
fetchData();
}, []); // studyNo가 바뀌면 다시 불러오도록 [studyNo]를 넣을 수도 있습니다.
// [1] 수정 페이지에 필요한 모든 데이터를 조회하는 함수
const fetchData = async () => {
try {
// 1. 장르 목록 전체 조회 (select box용)
const genreRes = await genreApi.getAll();
dispatch(setGenres(genreRes.data));
// 2. 수정할 주제(Study)의 상세 정보 조회
const studyRes = await studyApi.getIndi(studyNo);
setStudyData(studyRes.data); // 조회한 데이터로 studyData 상태 업데이트
// 3. 예문(Exam) 전체 조회 후, 현재 studyNo에 해당하는 예문만 필터링
const examRes = await examApi.getAll();
const studyExams = examRes.data.filter(exam => exam.studyNo == parseInt(studyNo));
// 4. 음성(Audio) 전체 조회
const audioRes = await audioApi.getAll();
// 5. 필터링된 예문 목록을 순회하며, 각 예문에 해당하는 음성 파일들을 찾아 추가합니다.
const examsWithAudios = studyExams.map(exam => ({
...exam, // 기존 예문 정보 (examKo, imagePath 등)
audioFiles: audioRes.data.filter(audio => audio.examNo == exam.examNo), // 이 예문에 속한 음성 파일 목록
newImageFile: null, // 새로 업로드할 이미지 파일을 저장할 공간
newAudioFiles: [] // 새로 업로드할 음성 파일들을 저장할 공간
}));
setExamList(examsWithAudios); // 최종적으로 가공된 예문 목록으로 상태 업데이트
setLoading(false); // 데이터 로딩 완료
} catch (e) { /*...*/ }
}
// [2] 각종 입력 및 파일 변경 핸들러들
// (AdminStudyCreate.jsx의 핸들러들과 대부분 유사한 구조)
// ...
// [2-6] 기존 음성 파일 삭제 핸들러
const handleDeleteExistingAudio = async (examIndex, audioNo) => {
if (!window.confirm("이 음성 파일을 삭제하시겠습니까?")) return;
try {
await audioApi.delete(audioNo); // 1. 서버에 삭제 요청
alert("음성 파일이 삭제되었습니다.")
// 2. 화면에 즉시 반영하기 위해 컴포넌트의 지역 상태(examList)에서도 삭제
setExamList(e => {
const newList = [...e];
newList[examIndex].audioFiles = newList[examIndex].audioFiles.filter(audio => audio.audioNo !== audioNo);
return newList;
})
} catch (e) { /*...*/ }
}
// [2-8] 예문 삭제 핸들러 (기존 예문 vs 새로 추가한 예문)
const handleDeleteExam = async (index, examNo) => {
// examNo가 없다는 것은 아직 DB에 저장되지 않은, 이 페이지에서 새로 추가한 예문이라는 의미
if (!examNo) {
setExamList(e => e.filter((_, i) => i !== index)); // API 호출 없이 상태에서만 제거
return;
}
// examNo가 있으면 DB에 저장된 예문이므로, 확인 후 API 호출
if (!window.confirm("이 예문을 삭제하시겠습니까?")) return;
try {
await examApi.delete(examNo); // 서버에 삭제 요청
alert("예문이 삭제되었습니다.")
setExamList(e => e.filter((_, i) => i !== index)); // 상태에서도 제거
} catch (e) { /*...*/ }
}
// [3] 유효성 검사 함수
const validateData = () => { /*...*/ }
// [4] '수정 완료' 버튼 클릭 시 실행되는 메인 함수
const handleSubmit = async () => {
if (!validateData()) return;
try {
setLoading(true);
// 1. 주제(Study) 정보 수정 API 호출
await studyApi.update(studyData);
console.log("Study 수정 완료")
// 2. 예문 목록을 순회하며 각 예문을 처리
for (let i = 0; i < examList.length; i++) {
const exam = examList[i];
// ** 핵심 분기 로직 **
// exam.examNo의 존재 여부로 기존 예문인지, 새로 추가된 예문인지 판단
if (exam.examNo) {
// [기존 예문 수정]
await examApi.update({
...exam,
newImageFile: exam.newImageFile // 새로 추가된 이미지 파일 정보 포함
});
console.log(`Exam ${exam.examNo} 수정 완료`);
// 이 기존 예문에 새로 추가된 음성 파일들이 있다면, 생성 API 호출
if (exam.newAudioFiles && exam.newAudioFiles.length > 0) {
for (const audioFile of exam.newAudioFiles) {
await audioApi.create({ /* ... */ });
}
}
} else {
// [새 예문 생성]
const examResponse = await examApi.create({
...exam,
studyNo: parseInt(studyNo), // 부모인 study의 ID를 명시
imageFile: exam.newImageFile // (주의: create API는 imageFile 필드를 기대할 수 있음)
});
const createdExamNo = examResponse.data;
console.log(`새 Exam 생성 완료, examNo: ${createdExamNo}`);
// 이 새 예문에 추가된 음성 파일들이 있다면, 생성 API 호출
if (exam.newAudioFiles && exam.newAudioFiles.length > 0) {
for (const audioFile of exam.newAudioFiles) {
await audioApi.create({ examNo: createdExamNo, /* ... */ });
}
}
}
}
alert("교육이 성공적으로 수정되었습니다!");
navigate('/admin/study'); // 수정 완료 후 목록 페이지로 이동
} catch (e) { /*...*/ } finally { setLoading(false); }
};
// 로딩 중일 때 보여줄 UI
if (loading) {
return <div style={{ padding: '40px', textAlign: 'center' }}>로딩 중...</div>;
}
// JSX 렌더링 부분
return (<>
{/* ... */}
</>)
}
6.2. 핵심 분석
데이터 조회 및 가공 (
fetchData)- 수정 페이지의 가장 큰 특징은 폼을 보여주기 전에 필요한 모든 데이터를 서버에서 가져와야 한다는 점입니다.
Promise.all을 사용하지 않고 개별적으로await를 사용한 이유는,studyExams를 먼저 얻고, 그 다음에audioRes와 조합해야 하는 등 데이터 간의 의존성이 있기 때문일 수 있습니다. (이 코드에서는Promise.all로도 구현 가능합니다.)- 가장 중요한 부분은
examsWithAudios를 만드는 과정입니다. 서버에서 온exam데이터에newImageFile: null,newAudioFiles: []와 같이 UI 상호작용을 통해 변경될 값을 저장할 새로운 필드를 추가하여examList상태를 구성합니다. 이것이 수정 페이지 상태 관리의 핵심입니다.
생성과 수정을 가르는 기준: ID의 유무
handleSubmit함수의for루프 안에서if (exam.examNo)라는 조건문이 가장 중요합니다.examNo가 있다는 것은fetchData를 통해 서버에서 불러온 기존 예문이라는 뜻이므로,examApi.update()를 호출합니다.examNo가 없다는 것은 사용자가 '예문 추가' 버튼을 눌러 새로 만든 새 예문이라는 뜻이므로,examApi.create()를 호출합니다.- 이러한 패턴은
handleDeleteExam함수에서도 동일하게 사용됩니다.
상태의 분리: 기존 파일 vs 새로운 파일
- 하나의 예문 객체(
exam) 안에 기존 파일 정보(imagePath,audioFiles)와 사용자가 새로 추가한 파일 정보(newImageFile,newAudioFiles)가 분리되어 관리됩니다. - 이렇게 해야 UI에서 기존 파일을 보여주면서 동시에 새로 추가할 파일 목록도 보여줄 수 있고,
handleSubmit에서 어떤 API를 호출해야 할지 명확하게 알 수 있습니다.
- 하나의 예문 객체(
낙관적 업데이트 (Optimistic Update)
handleDeleteExistingAudio함수를 보면,await audioApi.delete(audioNo)로 서버에 요청을 보낸 뒤, 성공하면fetchAllData()로 전체 데이터를 다시 불러오는 대신setExamList(...)를 통해 컴포넌트의 지역 상태를 직접 수정합니다.- 서버 요청이 성공할 것이라고 '낙관적으로' 가정하고 UI를 즉시 업데이트하는 방식입니다. 전체 데이터를 다시 불러오는 것보다 훨씬 빠르고 사용자 경험이 좋습니다. 만약 API 호출이 실패하면
catch블록에서 원래대로 되돌리는 로직을 추가할 수도 있습니다.
반응형
'프로젝트 > 웹' 카테고리의 다른 글
| [React] 기타 컴포넌트 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (1) | 2025.10.16 |
|---|---|
| [React] 등록 컴포넌트 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] 전체 목록 컴포넌트 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] 관리자단 슬라이스 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] API 계층 분석 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] 프로젝트 구조 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |