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
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "@/app/styles/global.css";
import React from "react";
import { BrowserRouter } from "react-router-dom";

import QueryProvider from "../src/components/@shared/providers/QueryProvider";
import QueryProvider from "../src/app/providers/QueryProvider";

import type { Preview } from "@storybook/react";

Expand Down
13 changes: 12 additions & 1 deletion src/entities/community/DTO.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,23 @@ export interface CommunityPostDetailResponse {
message: string;
}

// 인기 게시글 TOP 10
// 인기 게시글 TOP 10 (기존)
export interface PopularPostsResponse {
success: boolean;
data: CommunityPostItem[];
message: string;
}

// 인기 게시글 주간/월간/전체
export interface PopularPostsByPeriodResponse {
success: boolean;
data: {
weekly: CommunityPostItem[];
monthly: CommunityPostItem[];
all: CommunityPostItem[];
};
message: string;
}

export type UserRole = "TEACHER" | "PROSPECTIVE_TEACHER" | "ADMIN";
export type CommentStatus = "PENDING" | "PROCESSED" | "REJECTED" | "YET";
13 changes: 12 additions & 1 deletion src/entities/community/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
LikeStatusRequest,
LikeStatusResponse,
PopularPostsResponse,
PopularPostsByPeriodResponse,
} from "./DTO";

/**
* 인기 게시글 조회
* 인기 게시글 조회 (기존)
*/
export const getPopularPosts = async (): Promise<PopularPostsResponse> => {
return apiCall<void, PopularPostsResponse>({
Expand All @@ -22,6 +23,16 @@ export const getPopularPosts = async (): Promise<PopularPostsResponse> => {
});
};

/**
* 인기 게시글 조회 (주간/월간/전체)
*/
export const getPopularPostsByPeriod = async (): Promise<PopularPostsByPeriodResponse> => {
return apiCall<void, PopularPostsByPeriodResponse>({
method: "GET",
path: API_PATHS.COMMUNITY.POST.POPULAR,
});
};

/**
* 게시글 목록 조회
* @param params 게시글 목록 조회 파라미터
Expand Down
1 change: 1 addition & 0 deletions src/entities/community/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from "./useCreatePost";
export * from "./useDeletePost";
export * from "./useLikeStatus";
export * from "./usePopularPosts";
export * from "./usePopularPostsByPeriod";
export * from "./useToggleLike";
14 changes: 14 additions & 0 deletions src/entities/community/hooks/usePopularPostsByPeriod.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useSuspenseQuery } from "@tanstack/react-query";

import { getPopularPostsByPeriod } from "@/entities/community/api";
import { DYNAMIC_CACHE_CONFIG } from "@/shared/config/query";

import { PopularPostsByPeriodResponse } from "../DTO.d";

export const usePopularPostsByPeriod = () => {
return useSuspenseQuery<PopularPostsByPeriodResponse>({
queryKey: ["popularPostsByPeriod"],
queryFn: getPopularPostsByPeriod,
...DYNAMIC_CACHE_CONFIG,
});
};
3 changes: 2 additions & 1 deletion src/entities/community/hooks/useToggleLike.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const useToggleLike = () => {
mutationFn: (postId) => toggleLike(postId),
onError: () => {
toast({
title: "좋아요 오류",
title: "좋아요 실패",
description: "잠시 후 다시 시도해주세요.",
variant: "destructive",
});
},
Expand Down
36 changes: 24 additions & 12 deletions src/entities/review/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,48 @@ import { SortType } from "./DTO.d";

import type {
InternshipReview,
InternshipReviewResponse,
LikeResponse,
PaginatedReviewResponse,
ReviewQueryParams,
WorkReview,
WorkReviewResponse,
} from "./DTO.d";

// ------------------------------------------------------------------------------

export const getWorkReviews = async (
kindergartenId: number,
sortType?: SortType
) => {
const queryParams = sortType ? `?sortType=${sortType}` : "";
return apiCall<null, WorkReviewResponse>({
params?: ReviewQueryParams
): Promise<PaginatedReviewResponse<WorkReview>> => {
const { page = 0, size = 10, sortType = SortType.LATEST } = params || {};

const queryParams = new URLSearchParams({
page: page.toString(),
size: size.toString(),
sortType: sortType.toString(),
});

return apiCall<null, PaginatedReviewResponse<WorkReview>>({
method: "GET",
path: API_PATHS.WORK.GET(kindergartenId) + queryParams,
path: `${API_PATHS.WORK.GET(kindergartenId)}?${queryParams.toString()}`,
withAuth: true,
});
};

export const getInternshipReviews = async (
kindergartenId: number,
sortType?: SortType
) => {
const queryParams = sortType ? `?sortType=${sortType}` : "";
return apiCall<null, InternshipReviewResponse>({
params?: ReviewQueryParams
): Promise<PaginatedReviewResponse<InternshipReview>> => {
const { page = 0, size = 10, sortType = SortType.LATEST } = params || {};

const queryParams = new URLSearchParams({
page: page.toString(),
size: size.toString(),
sortType: sortType.toString(),
});

return apiCall<null, PaginatedReviewResponse<InternshipReview>>({
method: "GET",
path: API_PATHS.INTERNSHIP.GET(kindergartenId) + queryParams,
path: `${API_PATHS.INTERNSHIP.GET(kindergartenId)}?${queryParams.toString()}`,
withAuth: true,
});
};
Expand Down
24 changes: 22 additions & 2 deletions src/entities/review/hooks/useDeleteInternshipReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,45 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";

import { getUserInfo } from "@/entities/user/api";
import { useToast } from "@/shared/hooks/useToast";
import { useRewardAd } from "@/shared/hooks/useFlutterCommunication";

import { deleteInternshipReview } from "../api";
import { LikeResponse } from "../DTO.d";

export const useDeleteInternshipReview = () => {
const queryClient = useQueryClient();
const { toast } = useToast();
const [showRewardAd] = useRewardAd();

return useMutation<
LikeResponse,
Error,
{
internshipReviewId: number;
kindergartenId?: number;
skipAd?: boolean; // 광고 스킵 옵션 (테스트용)
}
>({
mutationFn: ({ internshipReviewId }) =>
deleteInternshipReview(internshipReviewId),
mutationFn: async ({ internshipReviewId, skipAd = false }) => {
// 광고를 스킵하지 않는 경우 보상형 광고 표시
if (!skipAd) {
const adResult = await showRewardAd();

// 사용자가 광고를 중간에 닫은 경우에만 삭제 중단
if (adResult.status === "cancelled") {
throw new Error("광고를 끝까지 시청해야 리뷰를 삭제할 수 있습니다.");
}

// status가 success이지만 rewarded가 false인 경우 (광고 없음, 로드 실패 등)
// -> 사용자 책임이 아니므로 그냥 진행
// rewarded가 true인 경우 -> 광고 시청 완료, 진행
}

// 광고 시청 완료 또는 광고 없음 -> 리뷰 삭제
return deleteInternshipReview(internshipReviewId);
},
onSuccess: async (_, variables) => {
// 해당 유치원의 실습 리뷰 목록을 다시 불러오기
if (variables.kindergartenId) {
queryClient.invalidateQueries({
queryKey: ["internshipReviews", variables.kindergartenId.toString()],
Expand Down
23 changes: 22 additions & 1 deletion src/entities/review/hooks/useDeleteWorkReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,45 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";

import { getUserInfo } from "@/entities/user/api";
import { useToast } from "@/shared/hooks/useToast";
import { useRewardAd } from "@/shared/hooks/useFlutterCommunication";

import { deleteWorkReview } from "../api";
import { LikeResponse } from "../DTO.d";

export const useDeleteWorkReview = () => {
const queryClient = useQueryClient();
const { toast } = useToast();
const [showRewardAd] = useRewardAd();

return useMutation<
LikeResponse,
Error,
{
workReviewId: number;
kindergartenId?: number;
skipAd?: boolean; // 광고 스킵 옵션 (테스트용)
}
>({
mutationFn: ({ workReviewId }) => deleteWorkReview(workReviewId),
mutationFn: async ({ workReviewId, skipAd = false }) => {
// 광고를 스킵하지 않는 경우 보상형 광고 표시
if (!skipAd) {
const adResult = await showRewardAd();

// 사용자가 광고를 중간에 닫은 경우에만 삭제 중단
if (adResult.status === "cancelled") {
throw new Error("광고를 끝까지 시청해야 리뷰를 삭제할 수 있습니다.");
}

// status가 success이지만 rewarded가 false인 경우 (광고 없음, 로드 실패 등)
// -> 사용자 책임이 아니므로 그냥 진행
// rewarded가 true인 경우 -> 광고 시청 완료, 진행
}

// 광고 시청 완료 또는 광고 없음 -> 리뷰 삭제
return deleteWorkReview(workReviewId);
},
onSuccess: async (_, variables) => {
// 해당 유치원의 근무 리뷰 목록을 다시 불러오기
if (variables.kindergartenId) {
queryClient.invalidateQueries({
queryKey: ["workReviews", variables.kindergartenId.toString()],
Expand Down
88 changes: 83 additions & 5 deletions src/entities/review/hooks/useGetReview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import {
useSuspenseInfiniteQuery,
useSuspenseQuery,
} from "@tanstack/react-query";

import { DYNAMIC_CACHE_CONFIG } from "@/shared/config/query";
import { REVIEW_TYPES } from "@/shared/constants/review";
import { safeParseId } from "@/shared/utils/idValidation";

Expand Down Expand Up @@ -39,7 +43,7 @@ export const useWorkReviews = (kindergartenId: string, sortType?: SortType) => {
if (!numericId) {
return Promise.resolve({ content: [], totalPages: 0 });
}
return getWorkReviews(numericId, sortType);
return getWorkReviews(numericId, { sortType });
},
});
};
Expand All @@ -62,7 +66,7 @@ export const useInternshipReviews = (
if (!numericId) {
return Promise.resolve({ content: [], totalPages: 0 });
}
return getInternshipReviews(numericId, sortType);
return getInternshipReviews(numericId, { sortType });
},
});
};
Expand All @@ -87,7 +91,7 @@ export function useGetReview(
if (!numericId) {
return Promise.resolve({ content: [], totalPages: 0 });
}
return getWorkReviews(numericId, sortType);
return getWorkReviews(numericId, { sortType });
},
});

Expand All @@ -97,7 +101,7 @@ export function useGetReview(
if (!numericId) {
return Promise.resolve({ content: [], totalPages: 0 });
}
return getInternshipReviews(numericId, sortType);
return getInternshipReviews(numericId, { sortType });
},
});

Expand Down Expand Up @@ -167,3 +171,77 @@ export function useGetReview(
scores,
};
}

// ------------------------------------------------------------------------------

/**
* 특정 유치원의 근무 리뷰 무한 스크롤 훅
*/
export const useInfiniteWorkReviews = (
kindergartenId: string,
sortType?: SortType,
pageSize: number = 10
) => {
const numericId = safeParseId(kindergartenId);

return useSuspenseInfiniteQuery({
queryKey: ["workReviews", kindergartenId, sortType, "infinite", pageSize],
queryFn: ({ pageParam = 0 }) => {
if (!numericId) {
return Promise.resolve({ content: [], totalPages: 0 });
}
return getWorkReviews(numericId, {
page: pageParam as number,
size: pageSize,
sortType,
});
},
getNextPageParam: (lastPage, allPages) => {
if (!lastPage?.content || lastPage.content.length === 0) return undefined;
const currentPage = allPages.length - 1;
const isLastPage = currentPage + 1 >= lastPage.totalPages;
return isLastPage ? undefined : currentPage + 1;
},
initialPageParam: 0,
...DYNAMIC_CACHE_CONFIG,
});
};

/**
* 특정 유치원의 실습 리뷰 무한 스크롤 훅
*/
export const useInfiniteInternshipReviews = (
kindergartenId: string,
sortType?: SortType,
pageSize: number = 10
) => {
const numericId = safeParseId(kindergartenId);

return useSuspenseInfiniteQuery({
queryKey: [
"internshipReviews",
kindergartenId,
sortType,
"infinite",
pageSize,
],
queryFn: ({ pageParam = 0 }) => {
if (!numericId) {
return Promise.resolve({ content: [], totalPages: 0 });
}
return getInternshipReviews(numericId, {
page: pageParam as number,
size: pageSize,
sortType,
});
},
getNextPageParam: (lastPage, allPages) => {
if (!lastPage?.content || lastPage.content.length === 0) return undefined;
const currentPage = allPages.length - 1;
const isLastPage = currentPage + 1 >= lastPage.totalPages;
return isLastPage ? undefined : currentPage + 1;
},
initialPageParam: 0,
...DYNAMIC_CACHE_CONFIG,
});
};
2 changes: 1 addition & 1 deletion src/entities/review/hooks/useReviewLike.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function useReviewLike(type: string, reviewId: number) {
const queryKey =
type === REVIEW_TYPES.WORK ? "workReviews" : "internshipReviews";

const { mutate: handleLike, isPending } = useMutation({
const { mutateAsync: handleLike, isPending } = useMutation({
mutationFn: () => {
if (type === REVIEW_TYPES.WORK) {
return likeWorkReview(reviewId);
Expand Down
Loading