Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SEMESTERS } from '../api/semester';

const semesters = [
{ code: 'SPRING_25', startId: 1, endId: 2422 },
{ code: 'SUMMER_25', startId: 2562, endId: 2647 },
{ code: 'FALL_25', startId: 2648, endId: 5144 },
{ code: 'WINTER_25', startId: 5185, endId: 5253 },
// { code: "SPRING_26", startId: 5254, endId: 7862 },
];

export function useSemesterNameBySubjectId(subjectId: number) {
const semester = semesters.find(sem => subjectId >= sem.startId && subjectId <= sem.endId);

const semesterData = SEMESTERS.find(s => s.semesterCode === semester?.code);

if (!semesterData) {
return {
semesterCode: undefined,
semesterValue: undefined,
};
}

return semesterData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Wishes } from '@/shared/model/types.ts';
import useWishes, { InitWishes } from '@/entities/wishes/model/useWishes.ts';
import { IPreRealSeat } from '@/entities/seat/api/usePreRealSeats';
import { useJoinPreSeats } from '@/entities/subjectAggregate/lib/joinSubjects.ts';
import { WishesInfo } from '@/features/wish/model/useWishesInfo';

interface DetailWishes {
isPending: boolean;
Expand All @@ -11,18 +12,18 @@ interface DetailWishes {

type WishesWithSeat = Wishes | (Wishes & IPreRealSeat);

function useDetailWishes(subjectId: number): DetailWishes {
const { data: wishes } = useWishes();
function useDetailWishes(wishesInfo: WishesInfo): DetailWishes {
const { data: wishes } = useWishes(wishesInfo.semesterCode);
const data = useJoinPreSeats(wishes, InitWishes);

if (!data) return { isPending: true };

const detail = data?.find(basket => basket.subjectId === subjectId);
const detail = data?.find(basket => basket.subjectId === wishesInfo.subjectId);

return {
isPending: false,
data: detail,
isLastSemesterWish: !detail,
isLastSemesterWish: !!wishesInfo.semesterCode,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { WishesInfo } from '@/features/wish/model/useWishesInfo';
import useWishes, { InitWishes } from '@/entities/wishes/model/useWishes.ts';
import { useJoinPreSeats } from '@/entities/subjectAggregate/lib/joinSubjects.ts';
import useDetailWishes from '@/entities/subjectAggregate/model/useDetailWishes.ts';

// Todo: Subject 부분은 따로 분리하기
/** subjectId 에 대한 추천 과목을 반환합니다. */
function useRecommendWishes(subjectId: number) {
const { data: wishes } = useWishes();
const { data: wish } = useDetailWishes(subjectId);
function useRecommendWishes(wishesInfo: WishesInfo) {
const { data: wishes } = useWishes(wishesInfo.semesterCode);
const data = useJoinPreSeats(wishes, InitWishes);

// fixme: subjectCode 를 구하기 위한 용도.

Check warning on line 11 in packages/client/src/entities/subjectAggregate/model/useRecommendWishes.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Take the required action to fix the issue indicated by this comment.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRgHzS9VYAvVftZ1&open=AZ2RnRgHzS9VYAvVftZ1&pullRequest=348
const { data: wish } = useDetailWishes(wishesInfo);
const subjectCode = wish?.subjectCode ?? '';

if (!data) return { isPending: true };

const detail = data.filter(basket => basket.subjectCode === subjectCode && basket.subjectId !== subjectId);
const detail = data.filter(basket => basket.subjectCode === subjectCode && basket.subjectId !== wishesInfo.subjectId);

return {
isPending: false,
Expand Down
10 changes: 7 additions & 3 deletions packages/client/src/entities/wishes/api/wishes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetchJsonOnPublic, fetchOnAPI } from '@/shared/api/api.ts';
import { WishRegister } from '@/shared/model/types.ts';
import { BadRequestError } from '@/shared/lib/errors.ts';
import { BadRequestError, NotFoundError } from '@/shared/lib/errors.ts';

export interface WishesApiResponse {
baskets: { subjectId: number; totalCount: number }[];
Expand All @@ -25,8 +25,12 @@ export const fetchDetailRegisters = async (subjectId: number): Promise<DetailReg
const errorMessage = await response.text();
const parsedError = jsonParse(errorMessage);

if (parsedError?.status === '400 BAD_REQUEST') {
throw new BadRequestError(parsedError.code);
if (response.status === 400) {
throw new BadRequestError(parsedError.message ?? response.statusText);
}

if (response.status === 404) {
throw new NotFoundError(parsedError.message ?? response.statusText);
}

throw new Error(await response.text());
Expand Down
11 changes: 6 additions & 5 deletions packages/client/src/entities/wishes/model/useDetailRegisters.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { BadRequestError } from '@/shared/lib/errors.ts';
import { WishesInfo } from '@/features/wish/model/useWishesInfo';
import { fetchDetailRegisters } from '@/entities/wishes/api/wishes.ts';
import { BadRequestError, NotFoundError } from '@/shared/lib/errors.ts';

function useDetailRegisters(id: number) {
function useDetailRegisters(wishesInfo: WishesInfo) {
return useQuery({
queryKey: ['detail-registers', id],
queryFn: () => fetchDetailRegisters(id),
queryKey: ['detail-registers', wishesInfo.subjectId],
queryFn: () => fetchDetailRegisters(wishesInfo.subjectId),
staleTime: Infinity,
retry: retryCondition,
});
Expand All @@ -15,7 +16,7 @@ const retryCondition = (failureCount: number, error: Error) => {
if (failureCount >= 3) return false;

// error 따라서 재시도 여부 결정
return !(error instanceof BadRequestError);
return !(error instanceof BadRequestError || error instanceof NotFoundError);
};

export default useDetailRegisters;
27 changes: 27 additions & 0 deletions packages/client/src/features/wish/model/useWishesInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useParams } from 'react-router-dom';
import { useSemesterNameBySubjectId } from '@/entities/semester/model/useSemesterNameBySubjectId';

export interface WishesInfo {
subjectId: number;
semesterCode: string | undefined;
semesterValue: string | undefined;
isLastSemesterWish: boolean;
}

// 관심과목 상세 페이지에서 subjectId와 semesterCode를 가져오는 커스텀 훅
// 다른 훅을 사용할 때, 정보를 주입
function useWishesInfo(): WishesInfo {
const params = useParams();
const subjectId = Number(params.id ?? '-1');

const semester = useSemesterNameBySubjectId(subjectId);
const isLastSemesterWish = Boolean(semester.semesterCode); // undefined이면 현재 학기 관심과목

return {
subjectId,
...semester,
isLastSemesterWish,
};
}

export default useWishesInfo;
99 changes: 15 additions & 84 deletions packages/client/src/pages/wishlist/WishesDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,49 @@
import { Helmet } from 'react-helmet';
import { useParams } from 'react-router-dom';
import Table from '@/widgets/wishlist/Table';
import useWishesInfo from '@/features/wish/model/useWishesInfo';
import WishesBarChart from '@/widgets/wishlist/WishesBarChart.tsx';
import DepartmentDoughnut from '@/widgets/wishlist/DepartmentDoughnut.tsx';
import FavoriteButton from '@/features/filtering/ui/button/FavoriteButton.tsx';
import AlarmButton from '@/features/live/pin/ui/AlarmButton';
import { InitWishes } from '@/entities/wishes/model/useWishes.ts';
import SubjectDetail from '@/entities/subjects/ui/SubjectDetail.tsx';
import useDetailWishes from '@/entities/subjectAggregate/model/useDetailWishes.ts';
import useRecommendWishes from '@/entities/subjectAggregate/model/useRecommendWishes.ts';
import useDetailRegisters from '@/entities/wishes/model/useDetailRegisters.ts';
import { getWishesColor } from '@/shared/config/colors.ts';
import { Card, Flex, Grid, Heading, SupportingText } from '@allcll/allcll-ui';
import DepartmentDoughnut from '@/widgets/wishlist/DepartmentDoughnut';
import RecommendWishes from '@/widgets/wishlist/RecommendWishes';
import WishesDetailInfo from '@/widgets/wishlist/WishesDetailInfo';
import useDetailRegisters from '@/entities/wishes/model/useDetailRegisters';
import { Card, Grid } from '@allcll/allcll-ui';

function WishesDetail() {
const params = useParams();
const subjectId = Number(params.id ?? '-1');
const { data: wishes, isPending, isLastSemesterWish } = useDetailWishes(subjectId);
const { data: registers, error } = useDetailRegisters(subjectId);

const data = wishes ?? InitWishes;
const isWishesAvailable = wishes && 'totalCount' in wishes;

if (isPending || !data) {
return (
<>
<Helmet>
<title>ALLCLL | 관심과목 분석 상세</title>
<meta name="description" content="세종대학교 관심과목의 상세 정보를 확인해보세요." />
</Helmet>

<div className="min-h-screen bg-gray-50 flex justify-center items-center">
<h1 className="text-2xl font-bold">Loading...</h1>
</div>
</>
);
}
const wishesInfo = useWishesInfo();

const { error } = useDetailRegisters(wishesInfo);
if (error) throw error;

return (
<>
<Helmet>
<title>ALLCLL | 관심과목 분석 상세</title>
<meta name="description" content="세종대학교 관심과목의 상세 정보를 확인해보세요." />
</Helmet>

{isLastSemesterWish && (
{wishesInfo.isLastSemesterWish && (
<p className="bg-red-100 text-red-500 py-2 px-4 font-bold">
이번 학기의 과목이 아닙니다. 수강 신청에 유의해주세요.
{wishesInfo.semesterValue}학기의 과목 입니다. 수강 신청에 유의해주세요.
</p>
)}

{/*Fixme: div depth 최적화하기*/}
<div className="min-h-screen bg-gray-50">
<div className="p-6 max-w-5xl mx-auto">
<Card>
<Flex justify="justify-between" align="items-center">
<Heading level={1}>{data.subjectName}</Heading>

<Flex gap="gap-2">
<FavoriteButton subject={data} />
<AlarmButton subject={data} />
</Flex>
</Flex>
<SubjectDetail wishes={wishes} />
<WishesDetailInfo wishesInfo={wishesInfo} />

{/* Analytics Section */}
<Grid columns={{ md: 2, base: 1 }} gap="gap-6" className=" mt-6">
{/* Doughnut Chart */}
<Card className="p-6">
<DepartmentDoughnut
data={registers?.eachDepartmentRegisters ?? []}
majorName={data.departmentName ?? data.manageDeptNm}
/>
<DepartmentDoughnut wishesInfo={wishesInfo} />
</Card>

{/* Competition Analysis */}
<Card>
<Flex gap="gap-2" align="items-center" className="mb-4">
<Heading level={2}>관심과목 경쟁률 예상</Heading>
{isWishesAvailable && (
<p className={`${getWishesColor(data.totalCount ?? -1)} font-bold text-xl`}>
총 {data.totalCount}명
</p>
)}
</Flex>

<WishesBarChart />
<WishesBarChart wishesInfo={wishesInfo} />
</Card>
</Grid>

{/* Alternative Course Table */}
<Card>
<Heading level={2} className="mt-2">
대체과목 추천
</Heading>
<SupportingText>학수번호가 같은 과목을 알려드려요</SupportingText>

<div className="overflow-x-auto">
<RecommendationTable subjectId={subjectId} />
</div>
<RecommendWishes wishesInfo={wishesInfo} />
</Card>
</Card>
</div>
Expand All @@ -108,17 +52,4 @@ function WishesDetail() {
);
}

// Todo: 대체 과목 테이블, WishesTable 컴포넌트 합칠 수 있는지 확인
// 대체과목 테이블 컴포넌트
interface IRecommendationTableProps {
subjectId: number;
}

function RecommendationTable({ subjectId }: IRecommendationTableProps) {
const { data: recommend } = useRecommendWishes(subjectId);
const placeholder = { title: '추천할 대체 과목이 없습니다.' };

return <Table data={recommend ?? []} placeholder={placeholder} />;
}

export default WishesDetail;
26 changes: 23 additions & 3 deletions packages/client/src/widgets/wishlist/DepartmentDoughnut.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
import { useState } from 'react';
import { Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js/auto';
import useDepartments, { DepartmentDict, useDepartmentDict } from '@/entities/departments/api/useDepartments.ts';
import {
getCollegeDoughnutData,
getDoughnutData,
getDoughnutTotalCount,
getMajorDoughnutData,
getUniversityDoughnutData,
} from '@/features/wish/lib/doughnut';
import { WishesInfo } from '@/features/wish/model/useWishesInfo';
import useDepartments, { DepartmentDict, useDepartmentDict } from '@/entities/departments/api/useDepartments.ts';
import useDetailWishes from '@/entities/subjectAggregate/model/useDetailWishes';
import useDetailRegisters from '@/entities/wishes/model/useDetailRegisters';
import { InitWishes } from '@/entities/wishes/model/useWishes';
import LoadingWithMessage from '@/shared/ui/Loading';
import { WishRegister } from '@/shared/model/types.ts';
import { Flex, Heading, Label } from '@allcll/allcll-ui';

ChartJS.register(ArcElement, Tooltip, Legend, BarElement, CategoryScale, LinearScale);

interface DepartmentDoughnutProps {
wishesInfo: WishesInfo;
}

export enum DoughnutSelectType {
MAJOR = '전공/비전공',
UNIVERSITY = '대학',
DEPARTMENT = '학과',
COLLEGE = '학부',
}

function DepartmentDoughnut({ data, majorName }: Readonly<{ data?: WishRegister[]; majorName: string }>) {
function DepartmentDoughnut({ wishesInfo }: DepartmentDoughnutProps) {

Check warning on line 33 in packages/client/src/widgets/wishlist/DepartmentDoughnut.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRcYzS9VYAvVftZr&open=AZ2RnRcYzS9VYAvVftZr&pullRequest=348
const { data: wishes, isPending } = useDetailWishes(wishesInfo);
const { data: registers } = useDetailRegisters(wishesInfo);

const data = registers?.eachDepartmentRegisters ?? [];
const nonNullWishes = wishes ?? InitWishes;
const majorName = nonNullWishes.departmentName ?? nonNullWishes.manageDeptNm;

const [selectedFilter, setSelectedFilter] = useState<DoughnutSelectType>(DoughnutSelectType.MAJOR);
const { data: departmentData } = useDepartments();
const departmentDict = useDepartmentDict(departmentData);
Expand All @@ -48,13 +64,17 @@
</select>
</Flex>

{!totalCount ? (
{isPending ? (
<Flex justify="justify-center" align="items-center" className="h-48">
<LoadingWithMessage message="관심과목 현황을 불러오는 중입니다..." />
</Flex>
) : !totalCount ? (

Check warning on line 71 in packages/client/src/widgets/wishlist/DepartmentDoughnut.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRcYzS9VYAvVftZs&open=AZ2RnRcYzS9VYAvVftZs&pullRequest=348
<Flex justify="justify-center" align="items-center" className="h-48">
<p className="text-center text-gray-500 font-semibold">관심과목을 담은 사람이 없습니다.</p>
</Flex>
) : (
<Doughnut data={doughnutData} />
)}

Check warning on line 77 in packages/client/src/widgets/wishlist/DepartmentDoughnut.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRcYzS9VYAvVftZt&open=AZ2RnRcYzS9VYAvVftZt&pullRequest=348
</>
);
}
Expand Down
38 changes: 38 additions & 0 deletions packages/client/src/widgets/wishlist/RecommendWishes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Table from '@/widgets/wishlist/Table';
import { WishesInfo } from '@/features/wish/model/useWishesInfo';
import useRecommendWishes from '@/entities/subjectAggregate/model/useRecommendWishes.ts';
import { Heading, SupportingText } from '@allcll/allcll-ui';

interface RecommendWishesProps {
wishesInfo: WishesInfo;
}

function RecommendWishes({ wishesInfo }: RecommendWishesProps) {

Check warning on line 10 in packages/client/src/widgets/wishlist/RecommendWishes.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRe_zS9VYAvVftZu&open=AZ2RnRe_zS9VYAvVftZu&pullRequest=348
return (
<>
<Heading level={2} className="mt-2">
대체과목 추천
</Heading>
<SupportingText>학수번호가 같은 과목을 알려드려요</SupportingText>

<div className="overflow-x-auto">
<RecommendationTable wishesInfo={wishesInfo} />
</div>
</>
);
}

// Todo: 대체 과목 테이블, WishesTable 컴포넌트 합칠 수 있는지 확인

Check warning on line 25 in packages/client/src/widgets/wishlist/RecommendWishes.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRe_zS9VYAvVftZv&open=AZ2RnRe_zS9VYAvVftZv&pullRequest=348
// 대체과목 테이블 컴포넌트
interface IRecommendationTableProps {
wishesInfo: WishesInfo;
}

function RecommendationTable({ wishesInfo }: IRecommendationTableProps) {

Check warning on line 31 in packages/client/src/widgets/wishlist/RecommendWishes.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2RnRe_zS9VYAvVftZw&open=AZ2RnRe_zS9VYAvVftZw&pullRequest=348
const { data: recommend } = useRecommendWishes(wishesInfo);
const placeholder = { title: '추천할 대체 과목이 없습니다.' };

return <Table data={recommend ?? []} placeholder={placeholder} />;
}

export default RecommendWishes;
Loading
Loading