diff --git a/src/App.jsx b/src/App.jsx index 6a89ecb..f0792b9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -147,22 +147,19 @@ export default function App() { /> - } + element={} /> {/* 404: 알 수 없는 경로는 홈으로 리디렉션 */} - } - /> - + } /> - navigate(tab)} - /> + {/* 하단 네비게이션 바 - addChallenge 페이지에서는 숨김 */} + {location.pathname !== '/addChallenge' && ( + navigate(tab)} + /> + )} ); } diff --git a/src/components/screens/AddChallengeScreen.jsx b/src/components/screens/AddChallengeScreen.jsx index e21be50..7adc007 100644 --- a/src/components/screens/AddChallengeScreen.jsx +++ b/src/components/screens/AddChallengeScreen.jsx @@ -3,160 +3,385 @@ import { useDispatch } from 'react-redux'; import { setActiveTab } from '../../store/slices/appSlice'; import api from '../../api/axios'; +// 챌린지 타입 키워드 (백엔드 자동 인증 연동용) +const VALID_CHALLENGE_TYPES = [ + '따릉이', + '전기차', + '수소차', + '재활용센터', + '제로웨이스트', +]; + export default function AddChallengeScreen({ onNavigate }) { - const dispatch = useDispatch(); + const dispatch = useDispatch(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [descriptionError, setDescriptionError] = useState(''); - const navigate = (tab) => { - if (typeof onNavigate === 'function') return onNavigate(tab); - dispatch(setActiveTab(tab)); - }; + const navigate = (tab) => { + if (typeof onNavigate === 'function') return onNavigate(tab); + dispatch(setActiveTab(tab)); + }; + // Description 유효성 검사 함수 + const validateDescription = (description) => { + if (!description.trim()) { + return '설명을 입력해주세요.'; + } + const firstWord = description.trim().split(' ')[0]; + if (!VALID_CHALLENGE_TYPES.includes(firstWord)) { + return `설명은 다음 키워드 중 하나로 시작해야 합니다: ${VALID_CHALLENGE_TYPES.join( + ', ' + )}`; + } + + return ''; + }; + + // Description 입력 변경 핸들러 + const handleDescriptionChange = (e) => { + const value = e.target.value; + const validationError = validateDescription(value); + setDescriptionError(validationError); + }; const handleAddChallenge = async () => { - console.log("추가 버튼 클릭"); - const token = localStorage.getItem('token'); + console.log('추가 버튼 클릭'); + setError(''); - const challengeName = document.getElementById('challengeName').value; - const description = document.getElementById('description').value; + const challengeName = document + .getElementById('challengeName') + .value.trim(); + const description = document.getElementById('description').value.trim(); const memberCount = document.getElementById('memberCount').value; const success = document.getElementById('success').value; const pointAmount = document.getElementById('pointAmount').value; const deadline = document.getElementById('deadline').value; - if (challengeName === '' || description === '' || memberCount === '' || - success === '' || pointAmount === '' || deadline === '' - ) { - alert('비어있는 칸이 있습니다. 칸을 모두 채워주세요'); + // 필수 필드 검사 + if ( + !challengeName || + !description || + !memberCount || + !success || + !pointAmount || + !deadline + ) { + setError('비어있는 칸이 있습니다. 칸을 모두 채워주세요.'); return; } - + + // Description 유효성 검사 + const descError = validateDescription(description); + if (descError) { + setDescriptionError(descError); + setError('설명 형식을 확인해주세요.'); + return; + } + + // 숫자 필드 검증 + const memberCountNum = parseInt(memberCount, 10); + const successNum = parseInt(success, 10); + const pointAmountNum = parseInt(pointAmount, 10); + const deadlineNum = parseInt(deadline, 10); + + if ( + memberCountNum < 0 || + successNum <= 0 || + pointAmountNum <= 0 || + deadlineNum <= 0 + ) { + setError('숫자 값을 올바르게 입력해주세요.'); + return; + } + const data = { - challengeName: challengeName, - description: description, - memberCount: memberCount, - success: success, - pointAmount: pointAmount, - deadline: deadline + challengeName, + description, + memberCount: memberCountNum, + success: successNum, + pointAmount: pointAmountNum, + deadline: deadlineNum, }; + setIsLoading(true); + try { - const res = await api.post("/chalregis", data, { - headers: { Authorization: `Bearer ${token}` } - }); - console.log("챌린지 추가 응답:", res.data); - navigate('challenge') + const res = await api.post('/chalregis', data); + console.log('챌린지 추가 응답:', res.data); + + if (res.data.status === 'SUCCESS') { + alert('✅ 챌린지가 성공적으로 등록되었습니다!'); + navigate('challenge'); + } else { + setError(res.data.message || '챌린지 추가에 실패했습니다.'); + } } catch (err) { - console.error("챌린지 추가 실패", err.response || err); - alert('챌린지 추가 실패'); - } + console.error('챌린지 추가 실패', err.response || err); + if (err.response?.status === 401) { + setError('❌ 인증이 필요합니다. 다시 로그인해주세요.'); + } else if (err.response?.status === 400) { + setError('❌ 입력 형식이 올바르지 않습니다.'); + } else if (err.response?.data?.message) { + setError(`❌ ${err.response.data.message}`); + } else { + setError('❌ 챌린지 추가 중 오류가 발생했습니다.'); + } + } finally { + setIsLoading(false); + } }; + return ( + <> + {/* 뒤로가기 버튼이 있는 헤더 */} + + {/* 뒤로가기 버튼 */} + navigate('challenge')} + className='absolute left-4 top-1/2 -translate-y-1/2 p-2 hover:bg-white/20 rounded-full transition-colors' + aria-label='뒤로가기' + > + + + + - - - return ( - <> - - 챌린지 추가 - - 환영합니다 관리자님 👋 추가할 챌린지를 작성해 주세요. - - - - - 챌린지 작성 - - - - 챌린지명 - - - - - 설명 - - - - - 시작 인원수 - - - - - 성공 조건 - - - 번 / km / 원 + {/* 제목 */} + + 챌린지 추가 + + 환영합니다 관리자님 👋 + + - - - - 지급 포인트 - - - - - 기한 - - - 일 + + + + 추가할 챌린지를 작성해 주세요. + - - - - - 추가하기 - - - - - > - ); -} + + + 챌린지 작성 + + + {/* 전역 에러 메시지 */} + {error && ( + + {error} + + )} + + + + + 챌린지명 + + + + + + 설명 * + + + {/* Description 규칙 안내 */} + + 💡 설명은 따릉이,{' '} + 전기차, 수소차,{' '} + 재활용센터,{' '} + 제로웨이스트 중 하나로 시작해야 + 합니다. + + {/* Description 에러 메시지 */} + {descriptionError && ( + + {descriptionError} + + )} + + + + + 시작 인원수 + + + + 초기 참여 인원수는 0으로 고정됩니다. + + + + + + 성공 조건 + + + + + km / 원 + + + + 따릉이는 km, 충전/상점은 원(₩) 단위입니다. + + + + + + 지급 포인트 + + + + + + + 기한 + + + + 일 + + + + + + {isLoading ? ( + + + + + + 등록 중... + + ) : ( + '추가하기' + )} + + + + + {/* 챌린지 타입 안내 */} + + + 📋 챌린지 타입 안내 + + + + • 따릉이: 자전거 이용 챌린지 (거리 + 기준, km 단위) + + + • 전기차: 전기차 충전 챌린지 + (충전비용 기준, 원 단위) + + + • 수소차: 수소차 충전 챌린지 + (충전비용 기준, 원 단위) + + + • 재활용센터: 재활용센터 방문 + 챌린지 (구매금액 기준, 원 단위) + + + • 제로웨이스트: 제로웨이스트 상점 + 이용 챌린지 (구매금액 기준, 원 단위) + + + + 💡 사용자가 인증을 완료하면 백엔드에서 자동으로 챌린지 + 진행률이 업데이트됩니다. + + + + > + ); +}
- 환영합니다 관리자님 👋 추가할 챌린지를 작성해 주세요. -
+ 환영합니다 관리자님 👋 +
+ 추가할 챌린지를 작성해 주세요. +
+ 💡 설명은 따릉이,{' '} + 전기차, 수소차,{' '} + 재활용센터,{' '} + 제로웨이스트 중 하나로 시작해야 + 합니다. +
+ {descriptionError} +
+ 초기 참여 인원수는 0으로 고정됩니다. +
+ 따릉이는 km, 충전/상점은 원(₩) 단위입니다. +
+ 💡 사용자가 인증을 완료하면 백엔드에서 자동으로 챌린지 + 진행률이 업데이트됩니다. +