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
18 changes: 10 additions & 8 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import AccountEditTab from '@/pages/AdminPage/tabs/AccountEditTab/AccountEditTab
import LoginTab from '@/pages/AdminPage/auth/LoginTab/LoginTab';
import PrivateRoute from '@/pages/AdminPage/auth/PrivateRoute/PrivateRoute';
import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab';
import AnswerApplicationForm from './pages/AdminPage/application/answer/AnswerApplicationForm';
import CreateApplicationForm from './pages/AdminPage/application/CreateApplicationForm';
// TODO: 지원서 개발 완료 후 활성화
// import AnswerApplicationForm from '@/pages/AdminPage/application/answer/AnswerApplicationForm';
// import CreateApplicationForm from '@/pages/AdminPage/application/CreateApplicationForm';
Expand Down Expand Up @@ -73,10 +75,10 @@ const App = () => {
/>
{/*🔒 메인 브랜치에서는 접근 차단 (배포용 차단 목적)*/}
{/*develop-fe 브랜치에서는 접근 가능하도록 풀고 개발 예정*/}
{/*<Route*/}
{/* path='application-edit'*/}
{/* element={<CreateApplicationForm />}*/}
{/*/>*/}
<Route
path='application-edit'
element={<CreateApplicationForm />}
/>
</Route>
</Routes>
</PrivateRoute>
Expand All @@ -85,10 +87,10 @@ const App = () => {
/>
{/*🔒 사용자용 지원서 작성 페이지도 메인에서는 비활성화 처리 */}
{/*🛠 develop-fe에서는 다시 노출 예정*/}
{/*<Route*/}
{/* path='/application/:clubId'*/}
{/* element={<AnswerApplicationForm />}*/}
{/*/>*/}
<Route
path='/application/:clubId'
element={<AnswerApplicationForm />}
/>
<Route path='*' element={<Navigate to='/' replace />} />
</Routes>
</BrowserRouter>
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/apis/application/applyToClub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import API_BASE_URL from '@/constants/api';
import { AnswerItem } from '@/types/application';

export const applyToClub = async (
clubId: string,
answers: AnswerItem[],
) => {
try {
const response = await fetch(
`${API_BASE_URL}/api/club/${clubId}/apply`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
questions: [
...answers
]
}),
},
);

if (!response.ok) {
throw new Error('답변 제출에 실패했습니다.');
}

const result = await response.json();
return result.data;
} catch (error) {
console.error('답변 제출 중 오류 발생:', error);
throw error;
}
};

export default applyToClub;
3 changes: 2 additions & 1 deletion frontend/src/apis/application/getApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const getApplication = async (clubId: string) => {
try {
const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
console.error(`Failed to fetch: ${response.statusText}`)
throw new Error((await response.json()).message);
Comment on lines +7 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

응답 본문을 한 번만 읽도록 개선하세요.

response.json()을 두 번 호출하면 "Body has already been read" 오류가 발생할 수 있습니다. 응답 본문을 한 번만 읽고 재사용하세요.

-      console.error(`Failed to fetch: ${response.statusText}`)
-      throw new Error((await response.json()).message);
+      const errorData = await response.json();
+      console.error(`Failed to fetch: ${response.statusText}`, errorData);
+      throw new Error(errorData.message || response.statusText);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error(`Failed to fetch: ${response.statusText}`)
throw new Error((await response.json()).message);
if (!response.ok) {
const errorData = await response.json();
console.error(`Failed to fetch: ${response.statusText}`, errorData);
throw new Error(errorData.message || response.statusText);
}
🤖 Prompt for AI Agents
In frontend/src/apis/application/getApplication.ts around lines 7 to 8, the code
calls response.json() twice, which causes a "Body has already been read" error.
To fix this, read the response body once by awaiting response.json() into a
variable, then use that variable both for logging the error message and throwing
the new Error.

}

const result = await response.json();
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/constants/INITIAL_FORM_DATA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import { ApplicationFormData } from '@/types/application';
const INITIAL_FORM_DATA: ApplicationFormData = {
title: '',
questions: [
//맨 처음은 이름
{
id: 1,
title: '이름을 입력해주세요',
description: '지원자의 이름을 입력해주세요. (예: 홍길동)',
type: 'SHORT_TEXT',
options: { required: true },
items: [],
},
{
id: 2,
title: '',
description: '',
type: 'SHORT_TEXT',
options: { required: true },
items: [],
},
{
id: 2,
id: 3,
title: '',
description: '',
type: 'CHOICE',
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/hooks/useAnswers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ export const useAnswers = () => {
const updateSingleAnswer = (id: number, value: string) => {
setAnswers((prev) => [
...prev.filter((a) => a.id !== id),
{ id, answer: value },
{ id, value: value },
]);
};

const updateMultiAnswer = (id: number, values: string[]) => {
setAnswers((prev) => [
...prev.filter((a) => a.id !== id),
...values.map((v) => ({ id, answer: v })),
...values.map((v) => ({ id, value: v })),
]);
};

Expand All @@ -27,7 +27,7 @@ export const useAnswers = () => {
};

const getAnswersById = (id: number) =>
answers.filter((a) => a.id === id).map((a) => a.answer);
answers.filter((a) => a.id === id).map((a) => a.value);

return { onAnswerChange, getAnswersById };
return { onAnswerChange, getAnswersById, answers };
};
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const CreateApplicationForm = () => {
options={question.options}
items={question.items}
type={question.type}
readOnly={index === 0} //인덱스 0번은 이름을 위한 고정 부분이므로 수정 불가
onTitleChange={handleTitleChange(question.id)}
onDescriptionChange={handleDescriptionChange(question.id)}
onItemsChange={handleItemsChange(question.id)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { PageContainer } from '@/styles/PageContainer.styles';
import * as Styled from './AnswerApplicationForm.styles';
import Header from '@/components/common/Header/Header';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail';
import ClubProfile from '@/pages/ClubDetailPage/components/ClubProfile/ClubProfile';
import { useAnswers } from '@/hooks/useAnswers';
import QuestionAnswerer from '@/pages/AdminPage/application/components/QuestionAnswerer/QuestionAnswerer';
import { useGetApplication } from '@/hooks/queries/application/useGetApplication';
import { Question } from '@/types/application';
import Spinner from '@/components/common/Spinner/Spinner';
import applyToClub from '@/apis/application/applyToClub';

const AnswerApplicationForm = () => {
const { clubId } = useParams<{ clubId: string }>();
const navigate = useNavigate();
if (!clubId) return null;

const { data: clubDetail, error } = useGetClubDetail(clubId);
const { data: formData, isLoading, isError } = useGetApplication(clubId);
const { data: formData, isLoading, isError, error: applicationError } = useGetApplication(clubId);

const { onAnswerChange, getAnswersById } = useAnswers();
const { onAnswerChange, getAnswersById, answers } = useAnswers();

if (isLoading) return <Spinner />;

if (isError) {
alert(applicationError.message)
navigate(`/club/${clubId}`)
return <div>문제가 발생했어요. 잠시 후 다시 시도해 주세요.</div>;
}

if (error || isError) {
if (error) {
return <div>문제가 발생했어요. 잠시 후 다시 시도해 주세요.</div>;
}

Expand All @@ -34,6 +42,16 @@ const AnswerApplicationForm = () => {
);
}

const handleSubmit = async () => {
try {
await applyToClub(clubId, answers);
alert('답변이 성공적으로 제출되었습니다.');
// TODO: 필요시 페이지 이동 등 추가
} catch (e) {
alert('답변 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.');
}
};

return (
<>
<Header />
Expand All @@ -57,7 +75,7 @@ const AnswerApplicationForm = () => {
))}
</Styled.QuestionsWrapper>
<Styled.ButtonWrapper>
<Styled.submitButton>제출하기</Styled.submitButton>
<Styled.submitButton onClick={handleSubmit}>제출하기</Styled.submitButton>
</Styled.ButtonWrapper>
</PageContainer>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export const SelectionToggleButton = styled.button<{ active: boolean }>`
color 0.2s ease;
`;

export const QuestionWrapper = styled.div`
export const QuestionWrapper = styled.div<{readOnly?: boolean}>`
display: flex;
gap: 36px;
pointer-events: ${({ readOnly }) => (readOnly ? 'none' : 'auto')};
cursor: ${({ readOnly }) => (readOnly ? 'not-allowed' : 'auto')};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const QuestionBuilder = ({
options,
items,
type,
readOnly,
onTitleChange,
onItemsChange,
onDescriptionChange,
Expand Down Expand Up @@ -117,7 +118,7 @@ const QuestionBuilder = ({
};

return (
<Styled.QuestionWrapper>
<Styled.QuestionWrapper readOnly={readOnly}>
<Styled.QuestionMenu>
<Styled.RequiredToggleButton
onClick={() => onRequiredChange?.(!options?.required)}
Expand All @@ -133,7 +134,9 @@ const QuestionBuilder = ({
}}
/>
{renderSelectionToggle()}
<button onClick={() => onRemoveQuestion()}>삭제</button>
{!readOnly && (
<button onClick={() => onRemoveQuestion()}>삭제</button>
)}
</Styled.QuestionMenu>
<Styled.QuestionFieldContainer>
{renderFieldByQuestionType()}
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => {
if (tab.label === '계정 관리') {
alert('계정 관리 기능은 아직 준비 중이에요. ☺️');
return;
} else if (tab.label === '지원 관리') {
alert('동아리 지원 관리 기능은 곧 오픈돼요!\n조금만 기다려주세요 🚀');
return;
}
navigate(tab.path);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail';

interface ButtonProps {
recruitmentForm?: string;
presidentPhoneNumber?: string;
isRecruiting: boolean;
}

const Button = styled.button`
Expand Down Expand Up @@ -38,8 +37,7 @@ const Button = styled.button`
`;

const ClubApplyButton = ({
recruitmentForm,
presidentPhoneNumber,
isRecruiting
}: ButtonProps) => {
const { clubId } = useParams<{ clubId: string }>();
const trackEvent = useMixpanelTrack();
Expand All @@ -49,14 +47,11 @@ const ClubApplyButton = ({
trackEvent('Club Apply Button Clicked');

//TODO: 지원서를 작성한 동아리의 경우에만 리다이렉트
//navigate(`/application/${clubId}`);

// [x] FIXME: recruitmentForm 있을 때는 리다이렉트
if (presidentPhoneNumber) {
alert(`${presidentPhoneNumber} 으로 연락하여 지원해 주세요.`);
} else {
alert('모집이 마감되었습니다. 다음에 지원해 주세요.');
if (!isRecruiting) {
alert('지원모집이 마감되었습니다. 다음에 지원해 주세요.')
return;
}
navigate(`/application/${clubId}`);
};

return <Button onClick={handleClick}>지원하기</Button>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ const ClubDetailFooter = ({
<Styled.ClubDetailFooterContainer>
<DeadlineBadge deadlineText={deadlineText} />
<ClubApplyButton
{...(deadlineText !== '모집 마감' && {
recruitmentForm,
presidentPhoneNumber,
})}
isRecruiting={deadlineText !== '모집 마감'}
/>
</Styled.ClubDetailFooterContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ const ClubDetailHeader = ({
logo={logo}
/>
<ClubApplyButton
{...(deadlineText !== '모집 마감' && {
recruitmentForm,
presidentPhoneNumber,
})}
isRecruiting={deadlineText !== '모집 마감'}
/>
</Styled.ClubDetailHeaderContainer>
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Question {
}

export interface QuestionBuilderProps extends Question {
readOnly: boolean;
onTitleChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
onItemsChange?: (newItems: { value: string }[]) => void;
Expand Down Expand Up @@ -52,5 +53,5 @@ export interface ApplicationFormData {

export interface AnswerItem {
id: number;
answer: string;
value: string;
}