Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/constants/eventName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '동아리 활동 사진 수정 페이지',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/AdminPage/AdminRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -35,7 +35,7 @@ export default function AdminRoutes() {
path='applicants-list/:applicationFormId/:questionId'
element={<ApplicantDetailPage />}
/>
<Route path='club-intro' element={<ClubIntroTab />} />
<Route path='club-intro' element={<ClubIntroEditTab />} />
</Route>
</Routes>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}>();
Expand All @@ -37,12 +45,9 @@ const ApplicationEditTab = () => {

const [formData, setFormData] =
useState<ApplicationFormData>(INITIAL_FORM_DATA);

const [nextId, setNextId] = useState(1);

const [applicationFormMode, setApplicationFormMode] =
useState<ApplicationFormMode>(ApplicationFormMode.INTERNAL);

const [externalApplicationUrl, setExternalApplicationUrl] = useState('');

useEffect(() => {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -368,7 +366,7 @@ const ExternalApplicationComponent = ({
/>
</Styled.ExternalApplicationFormContainer>
<Styled.ExternalApplicationFormHint>
현재 구글폼, 네이버폼 링크만 제출가능합니다.
현재 구글폼, 네이버폼, 에브리타임 링크만 제출가능합니다.
</Styled.ExternalApplicationFormHint>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,32 @@ const DIVISION_LABELS: Record<string, string> = {
과동: '과동아리',
};

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<ClubDetail | null>();
const { mutate: updateClub } = useUpdateClubDetail();
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClubDetail | null>();
const { mutate: updateClub } = useUpdateClubDetail();
Expand Down Expand Up @@ -99,7 +99,7 @@ const ClubIntroTab = () => {

<ContentSection.Body>
<CustomTextArea
label={`📝 ${clubDetail?.name || '동아리'}를 소개할게요`}
label='동아리를 소개할게요'
placeholder='동아리 소개 문구를 입력해주세요'
value={introDescription}
onChange={(e) => setIntroDescription(e.target.value)}
Expand All @@ -108,7 +108,7 @@ const ClubIntroTab = () => {
/>

<CustomTextArea
label='🎯 이런 활동을 해요'
label='이런 활동을 해요'
placeholder='동아리에서 하는 활동 내용을 입력해주세요'
value={activityDescription}
onChange={(e) => setActivityDescription(e.target.value)}
Expand All @@ -119,7 +119,7 @@ const ClubIntroTab = () => {
<AwardEditor awards={awards} onChange={setAwards} />

<CustomTextArea
label='💡 이런 사람이 오면 좋아요'
label='이런 사람이 오면 좋아요'
placeholder='동아리에 어울리는 사람의 특성을 입력해주세요'
value={idealCandidate.content}
onChange={(e) =>
Expand All @@ -130,7 +130,7 @@ const ClubIntroTab = () => {
/>

<CustomTextArea
label='🎁 부원이 되면 이런 혜택이 있어요'
label='부원이 되면 이런 혜택이 있어요'
placeholder='동아리 부원이 누릴 수 있는 혜택을 입력해주세요'
value={benefits}
onChange={(e) => setBenefits(e.target.value)}
Expand All @@ -145,4 +145,4 @@ const ClubIntroTab = () => {
);
};

export default ClubIntroTab;
export default ClubIntroEditTab;
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -140,7 +140,7 @@ const AwardEditor = ({ awards, onChange }: AwardEditorProps) => {

return (
<Styled.Container>
<Styled.Label>🏆 이런 상을 받았어요</Styled.Label>
<Styled.Label>이런 상을 받았어요</Styled.Label>

<Styled.AddSemesterSection>
<CustomDropDown
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useEffect, useRef, useState } from 'react';
import deleteButton from '@/assets/images/icons/delete_button_icon.svg';
import { FAQ } from '@/types/club';
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<Record<string, HTMLInputElement | null>>({});

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 (
<Styled.Container>
<Styled.Label>자주 묻는 질문 (FAQ)</Styled.Label>

<Styled.AddButton onClick={handleAddFAQ}>+ FAQ 추가</Styled.AddButton>

{faqs.length === 0 ? (
<Styled.EmptyState>
FAQ를 추가하여 지원자들의 자주 묻는 질문에 답변해보세요.
</Styled.EmptyState>
) : (
<Styled.FAQList>
{faqs
.filter((faq): faq is FAQ & { id: string } => !!faq.id)
.map((faq, index) => (
<Styled.FAQItem key={faq.id}>
<Styled.FAQHeader>
<Styled.FAQNumber>Q{index + 1}</Styled.FAQNumber>
<Styled.RemoveButton onClick={() => handleRemoveFAQ(faq.id)}>
<img src={deleteButton} alt='삭제' />
</Styled.RemoveButton>
</Styled.FAQHeader>

<Styled.QuestionInput
ref={(element) => {
questionInputRefs.current[faq.id] = element;
}}
placeholder='질문을 입력하세요'
value={faq.question}
onChange={(event) =>
handleUpdateQuestion(faq.id, event.target.value)
}
maxLength={100}
/>

<Styled.AnswerTextArea
placeholder='답변을 입력하세요'
value={faq.answer}
onChange={(event) =>
handleUpdateAnswer(faq.id, event.target.value)
}
maxLength={300}
/>

<Styled.CharCount>
질문: {faq.question.length}/100 | 답변: {faq.answer.length}
/300
</Styled.CharCount>
</Styled.FAQItem>
))}
</Styled.FAQList>
)}
</Styled.Container>
);
};

export default FAQEditor;
Loading