Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT-23] 학과별 입시 기능 구현 #24

Merged
merged 30 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7f572b6
✨ feat(custom): 단과대, 학과 api 추가
qwer0114 Feb 14, 2025
3ffb4d9
💄 style(custom): scrollbar 추가
qwer0114 Feb 14, 2025
8ff54d8
✨ feat(custom): 캠퍼스별 소개 메시지 추가
qwer0114 Feb 14, 2025
576eef4
✨ feat(custom): 캠퍼스 조회 tanstack query 커스텀 훅 정의
qwer0114 Feb 14, 2025
7f11eef
✨ feat(custom): 학과 조회 tanstack query 커스텀 훅 추가
qwer0114 Feb 14, 2025
6006f69
✨ feat(custom): step 타입들 정의
qwer0114 Feb 14, 2025
78c7ee4
✨ feat(custom): 단과대, 학과 조회 api 타입 추가
qwer0114 Feb 14, 2025
11a2459
✨ feat(custom): 학과 입시 결과 조회 funnel step 추가
qwer0114 Feb 14, 2025
401f626
♻️ refactor(custom): step 타입 step 파일로 이동
qwer0114 Feb 14, 2025
a54d6b2
💄 style(custom): fixed -\> absolute 로 변경
qwer0114 Feb 14, 2025
7176b1c
✨ feat(custom): 학과 선택 추가
qwer0114 Feb 14, 2025
3a7d2b3
♻️ refactor(custom): preset button 들 PresetButtons 공통 컴포넌트로 변경
qwer0114 Feb 14, 2025
a10e1e7
✨ feat(custom): presetButtons 공통 컴포넌트 분리
qwer0114 Feb 14, 2025
39989c0
♻️ refactor(custom): type 확장
qwer0114 Feb 14, 2025
992fb9f
♻️ refactor(custom): chatsteps 타입 변경에 따른 폴더 위치 변경
qwer0114 Feb 14, 2025
74c3b9a
✨ feat(custom): 캠퍼스 선택 기능 추가
qwer0114 Feb 14, 2025
224f70b
✨ feat(custom): 단과대 선택 기능 추가
qwer0114 Feb 14, 2025
c9a7939
♻️ refactor(custom): chatstep 타입 위치 변경에 따른 변경
qwer0114 Feb 14, 2025
534f587
✨ feat(custom): favicon 추가
qwer0114 Feb 14, 2025
e5e8e4c
📦 chore(custom): favicon 아이콘 추가
qwer0114 Feb 14, 2025
7a7ebdb
✨ feat(custom): 출처 버튼 조건부 렌더링 추가
qwer0114 Feb 14, 2025
d92e5c1
💄 style(custom): mt 변경
qwer0114 Feb 14, 2025
cab3c6a
✨ feat(custom): reference button 보이지 않도록 수정
qwer0114 Feb 14, 2025
c1a3b57
✨ feat(custom): 학과 선택 기능 추가
qwer0114 Feb 14, 2025
9e98374
♻️ refactor(custom): 제네릭으로 타입 확장
qwer0114 Feb 15, 2025
7f9fa4e
✨ feat(custom): 제넥릭 타입 추가 및 APIErrorBoundary 추가
qwer0114 Feb 15, 2025
c90dc13
♻️ refactor(custom): 제네릭 타입 추가
qwer0114 Feb 15, 2025
668e0ac
♻️ refactor(custom): 단과대 상태 객체로 변경
qwer0114 Feb 15, 2025
bd8cdfb
♻️ refactor(custom): 학과 프롬프트 수정
qwer0114 Feb 15, 2025
aa6734b
💄 style(custom): hover 시 cursor-pointer 추가
qwer0114 Feb 15, 2025
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
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>마루에그</title>
</head>
<body>
<div id="root"></div>
Expand Down
Binary file added public/favicon.ico
Binary file not shown.
11 changes: 11 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { server_axiosInstance } from '@/api';
import { AdmissionType, ResponseDetailAdmissionType } from '@/types/admission-type';
import { ResponseCollegeType, ResponseDepartmentType } from '@/types/department';
import { DefaultPostQuestionParams, PostQuestionResponse } from '@/types/questions';

export const getAdmissionDetail = async (type: AdmissionType): Promise<ResponseDetailAdmissionType[]> => {
const response = await server_axiosInstance.get(`/api/admissions/details/${type}`);
return response.data;
};

export const getCollege = async (college: '인문캠퍼스' | '자연캠퍼스'): Promise<ResponseCollegeType[]> => {
const response = await server_axiosInstance.get(`api/campuses/colleges/campus/${college}`);
return response.data;
};

export const getDepartment = async (collegeId: number): Promise<ResponseDepartmentType[]> => {
const response = await server_axiosInstance.get(`api/campuses/departments/college/${collegeId}`);
return response.data;
};

export const postQuestion = async ({
category,
type,
Expand Down
2 changes: 1 addition & 1 deletion src/components/bottom-sheet/bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function BottomSheet({ open, onClose, children }: BottomSheetProps) {
>
<div
onClick={(e) => e.stopPropagation()}
className={`absolute bottom-0 z-40 h-80 w-full overflow-y-auto rounded-t-2xl bg-white px-4 py-6 transition-transform duration-300 ${animationTrigger ? 'translate-y-0' : 'translate-y-full'}`}
className={`scrollbar absolute bottom-0 z-40 h-80 w-full overflow-y-auto rounded-t-2xl bg-white px-4 py-6 transition-transform duration-300 ${animationTrigger ? 'translate-y-0' : 'translate-y-full'}`}
>
{children}
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/menu-items/check-box/check-box.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { CheckFalseIcon, CheckTrueIcon } from '@/assets/svg';
import { cn } from '@/utils/style';

interface CheckboxProps {
label: string;
Expand All @@ -19,7 +20,7 @@ function Checkbox({ label, onClick, disabled = false }: CheckboxProps) {

return (
<label htmlFor={label} onClick={handleClick}>
<div className="relative px-4 py-3 hover:bg-gray-50">
<div className={cn('relative px-4 py-3', 'hover:rounded-md hover:bg-gray-50', 'cursor-pointer')}>
<input id={label} type="checkbox" className="sr-only" disabled={disabled}></input>
<div className="flex items-center gap-2">
{checked ? <CheckTrueIcon /> : <CheckFalseIcon />}
Expand Down
5 changes: 5 additions & 0 deletions src/constants/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ const systemMessage = {
additionalInfo:
'아래 버튼을 눌러 더 자세한 정보를 확인\n하거나 직접 질문해보세요!\n\n더욱 자세한 상담을 원하시면\n 명지대학교 입학처 02-300-1799,1800으로 전화주시길 바랍니다.',
referenceGuide: '💡답변 출처를 알려드릴게요!\n출처를 클릭하면 모집요강으로\n확인할 수 있어요!',
noReferenceGuide: '해당 답변에 대한 출처가 존재하지 않아요!',
errorMessage: '서버가 답변을 불러오는데 실패했어요.',
campusGuide:
'학과별로 보고싶으신가요?\n명지대학교는 총 2개의 캠퍼스,12개의\n단과대와 63개의 학과(부)로 구성되어\n있습니다.\n아래 질문들을 통해서 궁금하신 학과(부)를\n찾아주세요!',
seoulCampusIntro:
'명지대학교 인문캠퍼스는 문과 계열\n전체와 일부 ICT 융합대학과가 존재하는\n문과 중심의 캠퍼스입니다.\n\n단과대를 선택해주세요',
yonginCampusIntro:
'명지대학교 자연캠퍼스는 이과 계열\n전체와 예술체육대학을 중심으로 존재하는\n공과대 중심의 캠퍼스입니다.\n\n단과대를 선택해주세요',
} as const;

export default systemMessage;
11 changes: 11 additions & 0 deletions src/hooks/querys/useCollege.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getCollege } from '@/api/api';
import { useSuspenseQuery } from '@tanstack/react-query';

export default function useCollege(campus: '인문캠퍼스' | '자연캠퍼스') {
const { data } = useSuspenseQuery({
queryKey: [campus],
queryFn: () => getCollege(campus),
});

return data;
}
11 changes: 11 additions & 0 deletions src/hooks/querys/useDepartment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getDepartment } from '@/api/api';
import { useSuspenseQuery } from '@tanstack/react-query';

export default function useDepartment(collegeId: number) {
const { data } = useSuspenseQuery({
queryKey: [collegeId, 'COLLEGE_ID'],
queryFn: () => getDepartment(collegeId),
});

return data;
}
2 changes: 1 addition & 1 deletion src/hooks/use-question-form-hook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import useAdmissionStore from '@/stores/store/admission-store';
import useMessagesStore from '@/stores/store/message-store';
import { ChatSteps } from '@/types/chat';
import { ChatSteps } from '@/types/steps';
import { apiEventGATrigger } from '@/utils/ga-trigger';
import { useIsMutating } from '@tanstack/react-query';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import PresetButton from '@/components/preset-button/preset-button';
import PRESET_BUTTON, { AdmissionPresetType } from '@/constants/preset-buttons';
import { AdmissionPresetType } from '@/constants/preset-buttons';
import useAdmissionQuestionResult from '@/hooks/use-admission-question-result';
import PresetButtons from '@/page/components/preset-buttons/preset-buttons';
import useAdmissionStore from '@/stores/store/admission-store';
import useMessagesStore from '@/stores/store/message-store';
import { AdmissionType } from '@/types/admission-type';
import { ChatSteps } from '@/types/chat';
import { apiEventGATrigger } from '@/utils/ga-trigger';
import { ChatSteps } from '@/types/steps';

interface Props {
changeStep: (step: ChatSteps) => void;
Expand All @@ -23,77 +22,13 @@ function AdmissionCategoryResult({ admissionType, changeStep, admissionCategory
});
const { setMessages } = useMessagesStore();

const selectQuestion = (question: AdmissionPresetType) => {
const handleQuestionSelect = (question: AdmissionPresetType) => {
changeStep('상세전형 질문 결과');
setMessages([{ role: 'user', message: question.label }]);
setQuestion(question);
};

if (!isLoading)
return (
<div>
<div className="mt-2 flex w-full justify-end">
<div className="flex w-72 flex-wrap justify-end gap-2">
<PresetButton
onClick={() => {
changeStep('질문 출처 결과');
setMessages([{ role: 'user', message: '🙋‍♂️ 어디에서 볼 수 있나요?' }]);
apiEventGATrigger({
category: 'first preset button click',
action: 'click',
label: '첫 질문의 출처확인하기',
value: 1,
});
}}
>
🙋‍♂️ 어디에서 볼 수 있나요?
</PresetButton>
{PRESET_BUTTON.map((question) => (
<PresetButton
onClick={() => {
selectQuestion(question);
apiEventGATrigger({
category: 'first preset button click',
action: 'click',
label: `첫 질문의 ${question}질문`,
value: 1,
});
}}
key={question.label}
>
{question.label}
</PresetButton>
))}
<PresetButton
onClick={() => {
changeStep('상세전형 학과별 입시');
apiEventGATrigger({
category: 'first preset button click',
action: 'click',
label: `학과별 입시결과 확인`,
value: 1,
});
}}
>
학과별 입시
</PresetButton>
<PresetButton
onClick={() => {
window.location.reload();
apiEventGATrigger({
category: 'first preset button click',
action: 'click',
label: `조건 재설정`,
value: 1,
});
}}
>
조건 재설정
</PresetButton>
</div>
</div>
</div>
);
if (!isLoading) return <PresetButtons changeStep={changeStep} handleQuestionSelect={handleQuestionSelect} />;
}

export default AdmissionCategoryResult;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import DraggableScroller from '@/page/components/chat-step/choose-admission-cate
import useAdmissionStore from '@/stores/store/admission-store';
import useMessagesStore from '@/stores/store/message-store';
import { ADDMISSION, AdmissionType } from '@/types/admission-type';
import { ChatSteps } from '@/types/chat';
import { ChatSteps } from '@/types/steps';
import { apiEventGATrigger } from '@/utils/ga-trigger';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PresetButton from '@/components/preset-button/preset-button';
import useAdmissionStore from '@/stores/store/admission-store';
import useMessagesStore from '@/stores/store/message-store';
import { ADDMISSION, AdmissionType } from '@/types/admission-type';
import { ChatSteps } from '@/types/chat';
import { ChatSteps } from '@/types/steps';
import { apiEventGATrigger } from '@/utils/ga-trigger';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Suspense, useState } from 'react';
import APIErrorBoundary from '@/components/error/api-error-boundary';
import ChooseCampus from '@/page/components/chat-step/choose-department/steps/choose-campuse';
import ChooseCollege from '@/page/components/chat-step/choose-department/steps/choose-college';
import ChooseDepartment from '@/page/components/chat-step/choose-department/steps/choose-department';
import Funnel from '@/page/components/funnel/funnel';
import { ResponseCollegeType } from '@/types/department';
import { DepartmentSteps } from '@/types/steps';
import { ChatSteps } from '@/types/steps';

interface ChooseDepartmentStepsProps {
changeStep: (step: ChatSteps) => void;
}

function ChooseDepartmentSteps({ changeStep }: ChooseDepartmentStepsProps) {
const [steps, setSteps] = useState<DepartmentSteps>('캠퍼스 선택');
const [campus, setCampus] = useState<'인문캠퍼스' | '자연캠퍼스' | null>(null);
const [college, setCollege] = useState<ResponseCollegeType | null>(null);

const changeDepartMentStep = (step: DepartmentSteps) => {
setSteps(step);
};

return (
<Funnel<DepartmentSteps> step={steps}>
<Funnel.Step<DepartmentSteps> step="캠퍼스 선택">
<ChooseCampus changeDepartMentStep={changeDepartMentStep} setCampus={setCampus} />
</Funnel.Step>
<Funnel.Step<DepartmentSteps> step="단과대 선택">
<APIErrorBoundary>
<Suspense>
<ChooseCollege changeDepartMentStep={changeDepartMentStep} campus={campus!} setCollege={setCollege} />
</Suspense>
</APIErrorBoundary>
</Funnel.Step>
<Funnel.Step<DepartmentSteps> step="학과 선택">
<APIErrorBoundary>
<Suspense>
<ChooseDepartment changeStep={changeStep} college={college!} />
</Suspense>
</APIErrorBoundary>
</Funnel.Step>
</Funnel>
);
}

export default ChooseDepartmentSteps;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect } from 'react';
import Checkbox from '@/components/menu-items/check-box/check-box';
import MenuList from '@/components/menu-list/menu-list';
import systemMessage from '@/constants/message';
import useMessagesStore from '@/stores/store/message-store';
import { DepartmentSteps } from '@/types/steps';

interface ChooseCampusProps {
changeDepartMentStep: (step: DepartmentSteps) => void;
setCampus: (campus: '인문캠퍼스' | '자연캠퍼스') => void;
}

function ChooseCampus({ changeDepartMentStep, setCampus }: ChooseCampusProps) {
const { setMessages } = useMessagesStore();

useEffect(() => {
setMessages([{ role: 'system', message: systemMessage.campusGuide }]);
}, []);

const handleClick = (campus: '인문캠퍼스' | '자연캠퍼스') => {
changeDepartMentStep('단과대 선택');
setCampus(campus);
setMessages([{ role: 'user', message: campus }]);
};

return (
<div className="mt-3">
<MenuList>
<MenuList.Title title="캠퍼스를 선택해주세요"></MenuList.Title>
<Checkbox label="인문캠퍼스" onClick={() => handleClick('인문캠퍼스')} />
<Checkbox label="자연캠퍼스" onClick={() => handleClick('자연캠퍼스')} />
</MenuList>
</div>
);
}

export default ChooseCampus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffect } from 'react';
import Checkbox from '@/components/menu-items/check-box/check-box';
import MenuList from '@/components/menu-list/menu-list';
import systemMessage from '@/constants/message';
import useCollege from '@/hooks/querys/useCollege';
import useMessagesStore from '@/stores/store/message-store';
import { ResponseCollegeType } from '@/types/department';
import { DepartmentSteps } from '@/types/steps';

interface ChooseCollegeProps {
changeDepartMentStep: (step: DepartmentSteps) => void;
campus: '인문캠퍼스' | '자연캠퍼스';
setCollege: (college: ResponseCollegeType) => void;
}
function ChooseCollege({ changeDepartMentStep, campus, setCollege }: ChooseCollegeProps) {
const colleges = useCollege(campus);
const { setMessages } = useMessagesStore();

useEffect(() => {
const message = campus === '인문캠퍼스' ? systemMessage.seoulCampusIntro : systemMessage.yonginCampusIntro;
setMessages([{ role: 'system', message }]);
}, []);

const handleClick = (college: ResponseCollegeType) => {
changeDepartMentStep('학과 선택');
setCollege(college);
setMessages([{ role: 'user', message: college.name }]);
};

return (
<div className="mt-3">
<MenuList>
<MenuList.Title title="단과대학을 선택해주세요" />
{colleges.map((college) => (
<Checkbox label={college.name} key={college.collegeId} onClick={() => handleClick(college)} />
))}
</MenuList>
</div>
);
}

export default ChooseCollege;
Loading