diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index 0fd757188..f5d8a8774 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -97,6 +97,7 @@ export const PAGE_VIEW = { // 관리자 LOGIN_PAGE: '로그인페이지', + CLUB_INTRO_EDIT_PAGE: '동아리 소개 수정 페이지', CLUB_INFO_EDIT_PAGE: '동아리 기본 정보 수정 페이지', RECRUITMENT_INFO_EDIT_PAGE: '동아리 모집 정보 수정 페이지', PHOTO_EDIT_PAGE: '동아리 활동 사진 수정 페이지', diff --git a/frontend/src/pages/AdminPage/AdminRoutes.tsx b/frontend/src/pages/AdminPage/AdminRoutes.tsx index 758c047e4..8c5c7e687 100644 --- a/frontend/src/pages/AdminPage/AdminRoutes.tsx +++ b/frontend/src/pages/AdminPage/AdminRoutes.tsx @@ -9,7 +9,7 @@ import ClubInfoEditTab from '@/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEdit import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab'; import RecruitEditTab from '@/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab'; import ApplicantsTab from './tabs/ApplicantsTab/ApplicantsTab'; -import ClubIntroTab from './tabs/ClubIntroTab/ClubIntroTab'; +import ClubIntroEditTab from './tabs/ClubIntroEditTab/ClubIntroEditTab'; export default function AdminRoutes() { return ( @@ -35,7 +35,7 @@ export default function AdminRoutes() { path='applicants-list/:applicationFormId/:questionId' element={} /> - } /> + } /> ); diff --git a/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx index 37249bbae..b9b06f27d 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx @@ -20,9 +20,17 @@ import { import * as Styled from './ApplicationEditTab.styles'; import { QuestionDivider } from './ApplicationEditTab.styles'; +const externalApplicationUrlAllowed = [ + 'https://forms.gle', + 'https://docs.google.com/forms', + 'https://form.naver.com', + 'https://naver.me', + 'https://everytime.kr', +]; + const ApplicationEditTab = () => { - const navigate = useNavigate(); const queryClient = useQueryClient(); + const navigate = useNavigate(); const { applicationFormId: formId } = useParams<{ applicationFormId?: string; }>(); @@ -37,12 +45,9 @@ const ApplicationEditTab = () => { const [formData, setFormData] = useState(INITIAL_FORM_DATA); - const [nextId, setNextId] = useState(1); - const [applicationFormMode, setApplicationFormMode] = useState(ApplicationFormMode.INTERNAL); - const [externalApplicationUrl, setExternalApplicationUrl] = useState(''); useEffect(() => { @@ -129,20 +134,13 @@ const ApplicationEditTab = () => { if (applicationFormMode === ApplicationFormMode.INTERNAL) { payload.questions = reorderedQuestions; } else if (applicationFormMode === ApplicationFormMode.EXTERNAL) { - const externalApplicationUrlAllowed = [ - 'https://forms.gle', - 'https://docs.google.com/forms', - 'https://form.naver.com', - 'https://naver.me', - ]; - const isValidUrl = externalApplicationUrlAllowed.some((url) => externalApplicationUrl.startsWith(url), ); if (!isValidUrl) { alert( - '외부 지원서 링크는 Google Forms 또는 Naver Form 링크여야 합니다.', + '외부 지원서 링크는 Google Forms, Naver Form 또는 Everytime 링크여야 합니다.', ); return; } @@ -368,7 +366,7 @@ const ExternalApplicationComponent = ({ /> - 현재 구글폼, 네이버폼 링크만 제출가능합니다. + 현재 구글폼, 네이버폼, 에브리타임 링크만 제출가능합니다. ); diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx index bf9b26ad9..e748b6168 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx @@ -23,9 +23,32 @@ const DIVISION_LABELS: Record = { 과동: '과동아리', }; +const divisions = [ + { + value: '중동', + label: DIVISION_LABELS['중동'], + color: TAG_COLORS['중동'], + }, + { + value: '과동', + label: DIVISION_LABELS['과동'], + color: TAG_COLORS['과동'], + }, +]; + +const categories = [ + { value: '봉사', label: '봉사', color: TAG_COLORS['봉사'] }, + { value: '종교', label: '종교', color: TAG_COLORS['종교'] }, + { value: '취미교양', label: '취미교양', color: TAG_COLORS['취미교양'] }, + { value: '학술', label: '학술', color: TAG_COLORS['학술'] }, + { value: '운동', label: '운동', color: TAG_COLORS['운동'] }, + { value: '공연', label: '공연', color: TAG_COLORS['공연'] }, +]; + const ClubInfoEditTab = () => { const trackEvent = useMixpanelTrack(); useTrackPageView(PAGE_VIEW.CLUB_INFO_EDIT_PAGE); + const queryClient = useQueryClient(); const clubDetail = useOutletContext(); const { mutate: updateClub } = useUpdateClubDetail(); @@ -49,28 +72,6 @@ const ClubInfoEditTab = () => { x: '', }); - const queryClient = useQueryClient(); - const divisions = [ - { - value: '중동', - label: DIVISION_LABELS['중동'], - color: TAG_COLORS['중동'], - }, - { - value: '과동', - label: DIVISION_LABELS['과동'], - color: TAG_COLORS['과동'], - }, - ]; - const categories = [ - { value: '봉사', label: '봉사', color: TAG_COLORS['봉사'] }, - { value: '종교', label: '종교', color: TAG_COLORS['종교'] }, - { value: '취미교양', label: '취미교양', color: TAG_COLORS['취미교양'] }, - { value: '학술', label: '학술', color: TAG_COLORS['학술'] }, - { value: '운동', label: '운동', color: TAG_COLORS['운동'] }, - { value: '공연', label: '공연', color: TAG_COLORS['공연'] }, - ]; - useEffect(() => { if (clubDetail) { setClubName(clubDetail.name); diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/ClubIntroTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/tabs/ClubIntroTab/ClubIntroTab.styles.ts rename to frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.styles.ts diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/ClubIntroTab.tsx b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsx similarity index 91% rename from frontend/src/pages/AdminPage/tabs/ClubIntroTab/ClubIntroTab.tsx rename to frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsx index 60274a4d4..09b58222c 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/ClubIntroTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsx @@ -9,13 +9,13 @@ import useMixpanelTrack from '@/hooks/useMixpanelTrack'; import useTrackPageView from '@/hooks/useTrackPageView'; import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; import { Award, ClubDetail, FAQ, IdealCandidate } from '@/types/club'; -import * as Styled from './ClubIntroTab.styles'; +import * as Styled from './ClubIntroEditTab.styles'; import AwardEditor from './components/AwardEditor/AwardEditor'; import FAQEditor from './components/FAQEditor/FAQEditor'; -const ClubIntroTab = () => { +const ClubIntroEditTab = () => { const trackEvent = useMixpanelTrack(); - useTrackPageView(PAGE_VIEW.CLUB_INFO_EDIT_PAGE); + useTrackPageView(PAGE_VIEW.CLUB_INTRO_EDIT_PAGE); const clubDetail = useOutletContext(); const { mutate: updateClub } = useUpdateClubDetail(); @@ -99,7 +99,7 @@ const ClubIntroTab = () => { setIntroDescription(e.target.value)} @@ -108,7 +108,7 @@ const ClubIntroTab = () => { /> setActivityDescription(e.target.value)} @@ -119,7 +119,7 @@ const ClubIntroTab = () => { @@ -130,7 +130,7 @@ const ClubIntroTab = () => { /> setBenefits(e.target.value)} @@ -145,4 +145,4 @@ const ClubIntroTab = () => { ); }; -export default ClubIntroTab; +export default ClubIntroEditTab; diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/AwardEditor/AwardEditor.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/AwardEditor/AwardEditor.styles.ts rename to frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.styles.ts diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/AwardEditor/AwardEditor.tsx b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.tsx similarity index 98% rename from frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/AwardEditor/AwardEditor.tsx rename to frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.tsx index 8e172511b..9737129b0 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/AwardEditor/AwardEditor.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/AwardEditor/AwardEditor.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import deleteButton from '@/assets/images/icons/delete_button_icon.svg'; import selectIcon from '@/assets/images/icons/selectArrow.svg'; import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDown'; -import { Award } from '../../ClubIntroTab'; +import { Award } from '@/types/club'; import * as Styled from './AwardEditor.styles'; interface AwardEditorProps { @@ -140,7 +140,7 @@ const AwardEditor = ({ awards, onChange }: AwardEditorProps) => { return ( - 🏆 이런 상을 받았어요 + 이런 상을 받았어요 void; +} + +const FAQEditor = ({ faqs, onChange }: FAQEditorProps) => { + const [shouldFocusLast, setShouldFocusLast] = useState(false); + const questionInputRefs = useRef>({}); + + useEffect(() => { + const hasAnyMissingId = faqs.some((faq) => !faq.id); + if (hasAnyMissingId) { + const faqsWithIds = faqs.map((faq) => ({ + ...faq, + id: faq.id || `faq-${Date.now()}-${Math.random()}`, + })); + onChange(faqsWithIds); + } + }, [faqs, onChange]); + + const handleAddFAQ = () => { + const newFAQ: FAQ = { + id: `faq-${Date.now()}-${Math.random()}`, + question: '', + answer: '', + }; + onChange([...faqs, newFAQ]); + setShouldFocusLast(true); + }; + + const handleRemoveFAQ = (id: string) => { + onChange(faqs.filter((faq) => faq.id !== id)); + }; + + const handleUpdateQuestion = (id: string, value: string) => { + const updatedFAQs = faqs.map((faq) => + faq.id === id ? { ...faq, question: value } : faq, + ); + onChange(updatedFAQs); + }; + + const handleUpdateAnswer = (id: string, value: string) => { + const updatedFAQs = faqs.map((faq) => + faq.id === id ? { ...faq, answer: value } : faq, + ); + onChange(updatedFAQs); + }; + + useEffect(() => { + if (shouldFocusLast && faqs.length > 0) { + const lastFAQ = faqs[faqs.length - 1]; + if (lastFAQ.id) { + const inputRef = questionInputRefs.current[lastFAQ.id]; + if (inputRef) { + inputRef.focus(); + } + } + setShouldFocusLast(false); + } + }, [faqs, shouldFocusLast]); + + return ( + + 자주 묻는 질문 (FAQ) + + + FAQ 추가 + + {faqs.length === 0 ? ( + + FAQ를 추가하여 지원자들의 자주 묻는 질문에 답변해보세요. + + ) : ( + + {faqs + .filter((faq): faq is FAQ & { id: string } => !!faq.id) + .map((faq, index) => ( + + + Q{index + 1} + handleRemoveFAQ(faq.id)}> + 삭제 + + + + { + questionInputRefs.current[faq.id] = element; + }} + placeholder='질문을 입력하세요' + value={faq.question} + onChange={(event) => + handleUpdateQuestion(faq.id, event.target.value) + } + maxLength={100} + /> + + + handleUpdateAnswer(faq.id, event.target.value) + } + maxLength={300} + /> + + + 질문: {faq.question.length}/100 | 답변: {faq.answer.length} + /300 + + + ))} + + )} + + ); +}; + +export default FAQEditor; diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/FAQEditor/FAQEditor.tsx b/frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/FAQEditor/FAQEditor.tsx deleted file mode 100644 index 805963cea..000000000 --- a/frontend/src/pages/AdminPage/tabs/ClubIntroTab/components/FAQEditor/FAQEditor.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import deleteButton from '@/assets/images/icons/delete_button_icon.svg'; -import { FAQ } from '../../ClubIntroTab'; -import * as Styled from './FAQEditor.styles'; - -interface FAQEditorProps { - faqs: FAQ[]; - onChange: (faqs: FAQ[]) => void; -} - -const FAQEditor = ({ faqs, onChange }: FAQEditorProps) => { - const [shouldFocusLast, setShouldFocusLast] = useState(false); - const questionInputRefs = useRef>({}); - - const handleAddFAQ = () => { - const newFAQ: FAQ = { - id: `faq-${Date.now()}-${Math.random()}`, - question: '', - answer: '', - }; - onChange([...faqs, newFAQ]); - setShouldFocusLast(true); - }; - - const handleRemoveFAQ = (id: string) => { - onChange(faqs.filter((faq) => faq.id !== id)); - }; - - const handleUpdateQuestion = (id: string, value: string) => { - const updatedFAQs = faqs.map((faq) => - faq.id === id ? { ...faq, question: value } : faq, - ); - onChange(updatedFAQs); - }; - - const handleUpdateAnswer = (id: string, value: string) => { - const updatedFAQs = faqs.map((faq) => - faq.id === id ? { ...faq, answer: value } : faq, - ); - onChange(updatedFAQs); - }; - - useEffect(() => { - if (shouldFocusLast && faqs.length > 0) { - const lastFAQ = faqs[faqs.length - 1]; - const inputRef = questionInputRefs.current[lastFAQ.id]; - if (inputRef) { - inputRef.focus(); - } - setShouldFocusLast(false); - } - }, [faqs, shouldFocusLast]); - - return ( - - ❓ 자주 묻는 질문 (FAQ) - - + FAQ 추가 - - {faqs.length === 0 ? ( - - FAQ를 추가하여 지원자들의 자주 묻는 질문에 답변해보세요. - - ) : ( - - {faqs.map((faq, index) => ( - - - Q{index + 1} - handleRemoveFAQ(faq.id)}> - 삭제 - - - - { - questionInputRefs.current[faq.id] = element; - }} - placeholder='질문을 입력하세요' - value={faq.question} - onChange={(event) => - handleUpdateQuestion(faq.id, event.target.value) - } - maxLength={100} - /> - - - handleUpdateAnswer(faq.id, event.target.value) - } - maxLength={300} - /> - - - 질문: {faq.question.length}/100 | 답변: {faq.answer.length}/300 - - - ))} - - )} - - ); -}; - -export default FAQEditor; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts index dfaf2af63..655515ca5 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts @@ -210,6 +210,7 @@ export const AnswerBox = styled.div` ${setTypography(typography.paragraph.p3)}; color: ${colors.gray[800]}; line-height: 1.5; + white-space: pre-line; display: flex; gap: 8px; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx index d6d1ab702..a3ad1ab13 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx @@ -61,9 +61,10 @@ const ClubIntroContent = ({ )} + {awards && awards.length > 0 && ( - 🏆 동아리 성과 + 동아리 성과 {awards.map((award) => ( @@ -80,7 +81,6 @@ const ClubIntroContent = ({ )} - {idealCandidate?.content?.trim() && ( 이런 사람이 오면 좋아요 @@ -89,7 +89,6 @@ const ClubIntroContent = ({ )} - {benefits?.trim() && ( 동아리 부원이 가지는 혜택 @@ -98,7 +97,6 @@ const ClubIntroContent = ({ )} - {faqs && faqs.length > 0 && ( FAQ diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts index d38e5c3be..37f1e69b8 100644 --- a/frontend/src/types/club.ts +++ b/frontend/src/types/club.ts @@ -1,10 +1,6 @@ import { SNS_CONFIG } from '@/constants/snsConfig'; -export type RecruitmentStatus = - | 'OPEN' - | 'CLOSED' - | 'UPCOMING' - | 'ALWAYS'; +export type RecruitmentStatus = 'OPEN' | 'CLOSED' | 'UPCOMING' | 'ALWAYS'; export interface Club { id: string; @@ -56,6 +52,7 @@ export interface IdealCandidate { } export interface FAQ { + id?: string; question: string; answer: string; }