Skip to content

Commit 7259d0c

Browse files
authored
Merge pull request #536 from Moadong/feature/user-apply
[feature] 동아리 지원하기 기능
2 parents e429741 + ed44003 commit 7259d0c

File tree

14 files changed

+104
-45
lines changed

14 files changed

+104
-45
lines changed

frontend/src/App.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import AccountEditTab from '@/pages/AdminPage/tabs/AccountEditTab/AccountEditTab
1515
import LoginTab from '@/pages/AdminPage/auth/LoginTab/LoginTab';
1616
import PrivateRoute from '@/pages/AdminPage/auth/PrivateRoute/PrivateRoute';
1717
import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab';
18+
import AnswerApplicationForm from './pages/AdminPage/application/answer/AnswerApplicationForm';
19+
import CreateApplicationForm from './pages/AdminPage/application/CreateApplicationForm';
1820
// TODO: 지원서 개발 완료 후 활성화
1921
// import AnswerApplicationForm from '@/pages/AdminPage/application/answer/AnswerApplicationForm';
2022
// import CreateApplicationForm from '@/pages/AdminPage/application/CreateApplicationForm';
@@ -73,10 +75,10 @@ const App = () => {
7375
/>
7476
{/*🔒 메인 브랜치에서는 접근 차단 (배포용 차단 목적)*/}
7577
{/*develop-fe 브랜치에서는 접근 가능하도록 풀고 개발 예정*/}
76-
{/*<Route*/}
77-
{/* path='application-edit'*/}
78-
{/* element={<CreateApplicationForm />}*/}
79-
{/*/>*/}
78+
<Route
79+
path='application-edit'
80+
element={<CreateApplicationForm />}
81+
/>
8082
</Route>
8183
</Routes>
8284
</PrivateRoute>
@@ -85,10 +87,10 @@ const App = () => {
8587
/>
8688
{/*🔒 사용자용 지원서 작성 페이지도 메인에서는 비활성화 처리 */}
8789
{/*🛠 develop-fe에서는 다시 노출 예정*/}
88-
{/*<Route*/}
89-
{/* path='/application/:clubId'*/}
90-
{/* element={<AnswerApplicationForm />}*/}
91-
{/*/>*/}
90+
<Route
91+
path='/application/:clubId'
92+
element={<AnswerApplicationForm />}
93+
/>
9294
<Route path='*' element={<Navigate to='/' replace />} />
9395
</Routes>
9496
</BrowserRouter>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import API_BASE_URL from '@/constants/api';
2+
import { AnswerItem } from '@/types/application';
3+
4+
export const applyToClub = async (
5+
clubId: string,
6+
answers: AnswerItem[],
7+
) => {
8+
try {
9+
const response = await fetch(
10+
`${API_BASE_URL}/api/club/${clubId}/apply`,
11+
{
12+
method: 'POST',
13+
headers: {
14+
'Content-Type': 'application/json',
15+
},
16+
body: JSON.stringify({
17+
questions: [
18+
...answers
19+
]
20+
}),
21+
},
22+
);
23+
24+
if (!response.ok) {
25+
throw new Error('답변 제출에 실패했습니다.');
26+
}
27+
28+
const result = await response.json();
29+
return result.data;
30+
} catch (error) {
31+
console.error('답변 제출 중 오류 발생:', error);
32+
throw error;
33+
}
34+
};
35+
36+
export default applyToClub;

frontend/src/apis/application/getApplication.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ const getApplication = async (clubId: string) => {
44
try {
55
const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`);
66
if (!response.ok) {
7-
throw new Error(`Failed to fetch: ${response.statusText}`);
7+
console.error(`Failed to fetch: ${response.statusText}`)
8+
throw new Error((await response.json()).message);
89
}
910

1011
const result = await response.json();

frontend/src/constants/INITIAL_FORM_DATA.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,25 @@ import { ApplicationFormData } from '@/types/application';
33
const INITIAL_FORM_DATA: ApplicationFormData = {
44
title: '',
55
questions: [
6+
//맨 처음은 이름
67
{
78
id: 1,
9+
title: '이름을 입력해주세요',
10+
description: '지원자의 이름을 입력해주세요. (예: 홍길동)',
11+
type: 'SHORT_TEXT',
12+
options: { required: true },
13+
items: [],
14+
},
15+
{
16+
id: 2,
817
title: '',
918
description: '',
1019
type: 'SHORT_TEXT',
1120
options: { required: true },
1221
items: [],
1322
},
1423
{
15-
id: 2,
24+
id: 3,
1625
title: '',
1726
description: '',
1827
type: 'CHOICE',

frontend/src/hooks/useAnswers.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ export const useAnswers = () => {
77
const updateSingleAnswer = (id: number, value: string) => {
88
setAnswers((prev) => [
99
...prev.filter((a) => a.id !== id),
10-
{ id, answer: value },
10+
{ id, value: value },
1111
]);
1212
};
1313

1414
const updateMultiAnswer = (id: number, values: string[]) => {
1515
setAnswers((prev) => [
1616
...prev.filter((a) => a.id !== id),
17-
...values.map((v) => ({ id, answer: v })),
17+
...values.map((v) => ({ id, value: v })),
1818
]);
1919
};
2020

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

2929
const getAnswersById = (id: number) =>
30-
answers.filter((a) => a.id === id).map((a) => a.answer);
30+
answers.filter((a) => a.id === id).map((a) => a.value);
3131

32-
return { onAnswerChange, getAnswersById };
32+
return { onAnswerChange, getAnswersById, answers };
3333
};

frontend/src/pages/AdminPage/application/CreateApplicationForm.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const CreateApplicationForm = () => {
158158
options={question.options}
159159
items={question.items}
160160
type={question.type}
161+
readOnly={index === 0} //인덱스 0번은 이름을 위한 고정 부분이므로 수정 불가
161162
onTitleChange={handleTitleChange(question.id)}
162163
onDescriptionChange={handleDescriptionChange(question.id)}
163164
onItemsChange={handleItemsChange(question.id)}

frontend/src/pages/AdminPage/application/answer/AnswerApplicationForm.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
import { PageContainer } from '@/styles/PageContainer.styles';
22
import * as Styled from './AnswerApplicationForm.styles';
33
import Header from '@/components/common/Header/Header';
4-
import { useParams } from 'react-router-dom';
4+
import { useNavigate, useParams } from 'react-router-dom';
55
import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail';
66
import ClubProfile from '@/pages/ClubDetailPage/components/ClubProfile/ClubProfile';
77
import { useAnswers } from '@/hooks/useAnswers';
88
import QuestionAnswerer from '@/pages/AdminPage/application/components/QuestionAnswerer/QuestionAnswerer';
99
import { useGetApplication } from '@/hooks/queries/application/useGetApplication';
1010
import { Question } from '@/types/application';
1111
import Spinner from '@/components/common/Spinner/Spinner';
12+
import applyToClub from '@/apis/application/applyToClub';
1213

1314
const AnswerApplicationForm = () => {
1415
const { clubId } = useParams<{ clubId: string }>();
16+
const navigate = useNavigate();
1517
if (!clubId) return null;
1618

1719
const { data: clubDetail, error } = useGetClubDetail(clubId);
18-
const { data: formData, isLoading, isError } = useGetApplication(clubId);
20+
const { data: formData, isLoading, isError, error: applicationError } = useGetApplication(clubId);
1921

20-
const { onAnswerChange, getAnswersById } = useAnswers();
22+
const { onAnswerChange, getAnswersById, answers } = useAnswers();
2123

2224
if (isLoading) return <Spinner />;
25+
26+
if (isError) {
27+
alert(applicationError.message)
28+
navigate(`/club/${clubId}`)
29+
return <div>문제가 발생했어요. 잠시 후 다시 시도해 주세요.</div>;
30+
}
2331

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

@@ -34,6 +42,16 @@ const AnswerApplicationForm = () => {
3442
);
3543
}
3644

45+
const handleSubmit = async () => {
46+
try {
47+
await applyToClub(clubId, answers);
48+
alert('답변이 성공적으로 제출되었습니다.');
49+
// TODO: 필요시 페이지 이동 등 추가
50+
} catch (e) {
51+
alert('답변 제출에 실패했습니다. 잠시 후 다시 시도해 주세요.');
52+
}
53+
};
54+
3755
return (
3856
<>
3957
<Header />
@@ -57,7 +75,7 @@ const AnswerApplicationForm = () => {
5775
))}
5876
</Styled.QuestionsWrapper>
5977
<Styled.ButtonWrapper>
60-
<Styled.submitButton>제출하기</Styled.submitButton>
78+
<Styled.submitButton onClick={handleSubmit}>제출하기</Styled.submitButton>
6179
</Styled.ButtonWrapper>
6280
</PageContainer>
6381
</>

frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export const SelectionToggleButton = styled.button<{ active: boolean }>`
7575
color 0.2s ease;
7676
`;
7777

78-
export const QuestionWrapper = styled.div`
78+
export const QuestionWrapper = styled.div<{readOnly?: boolean}>`
7979
display: flex;
8080
gap: 36px;
81+
pointer-events: ${({ readOnly }) => (readOnly ? 'none' : 'auto')};
82+
cursor: ${({ readOnly }) => (readOnly ? 'not-allowed' : 'auto')};
8183
`;

frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const QuestionBuilder = ({
1616
options,
1717
items,
1818
type,
19+
readOnly,
1920
onTitleChange,
2021
onItemsChange,
2122
onDescriptionChange,
@@ -117,7 +118,7 @@ const QuestionBuilder = ({
117118
};
118119

119120
return (
120-
<Styled.QuestionWrapper>
121+
<Styled.QuestionWrapper readOnly={readOnly}>
121122
<Styled.QuestionMenu>
122123
<Styled.RequiredToggleButton
123124
onClick={() => onRequiredChange?.(!options?.required)}
@@ -133,7 +134,9 @@ const QuestionBuilder = ({
133134
}}
134135
/>
135136
{renderSelectionToggle()}
136-
<button onClick={() => onRemoveQuestion()}>삭제</button>
137+
{!readOnly && (
138+
<button onClick={() => onRemoveQuestion()}>삭제</button>
139+
)}
137140
</Styled.QuestionMenu>
138141
<Styled.QuestionFieldContainer>
139142
{renderFieldByQuestionType()}

frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => {
3131
if (tab.label === '계정 관리') {
3232
alert('계정 관리 기능은 아직 준비 중이에요. ☺️');
3333
return;
34-
} else if (tab.label === '지원 관리') {
35-
alert('동아리 지원 관리 기능은 곧 오픈돼요!\n조금만 기다려주세요 🚀');
36-
return;
3734
}
3835
navigate(tab.path);
3936
};

0 commit comments

Comments
 (0)