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
39 changes: 28 additions & 11 deletions app/(my)/my-study-review/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Image from 'next/image';
import { useState } from 'react';
import KeywordReview from '@/entities/user/ui/keyword-review';
import MoreKeywordReviewModal from '@/entities/user/ui/more-keyword-review-modal';
import { MyReviewItem } from '@/features/study/api/types';
import {
useMyNegativeKeywordsQuery,
Expand All @@ -15,6 +16,7 @@ export default function MyStudyReview() {
const { data: positiveKeywordsData } = useUserPositiveKeywordsQuery({
pageSize: 5,
});

const { data: negativeKeywordsData } = useMyNegativeKeywordsQuery({
pageSize: 5,
});
Expand Down Expand Up @@ -55,11 +57,7 @@ export default function MyStudyReview() {
<div className="mb-200 flex justify-between">
<h3 className="font-designer-16b text-text-default">좋았던 점</h3>

{positiveKeywords.length > 5 && (
<button className="font-designer-12m text-text-subtlest cursor-pointer">
더보기
</button>
)}
<MorePositiveKeywordsModal />
</div>

<ul className="flex flex-col gap-50">
Expand All @@ -85,11 +83,7 @@ export default function MyStudyReview() {
개선이 필요한 점
</h3>

{negativeKeywords.length > 5 && (
<button className="font-designer-12m text-text-subtlest cursor-pointer">
더보기
</button>
)}
<MoreNegativeKeywordsModal />
</div>

<ul className="flex flex-col gap-50">
Expand Down Expand Up @@ -153,6 +147,29 @@ export default function MyStudyReview() {
);
}

function MorePositiveKeywordsModal() {
const { data: allPositiveKeywordsData } = useUserPositiveKeywordsQuery({});

const allPositiveKeywords = allPositiveKeywordsData?.keywords || [];

return (
<MoreKeywordReviewModal title="좋았던 점" keywords={allPositiveKeywords} />
);
}

function MoreNegativeKeywordsModal() {
const { data: allNegativeKeywordsData } = useMyNegativeKeywordsQuery({});

const allNegativeKeywords = allNegativeKeywordsData?.keywords || [];

return (
<MoreKeywordReviewModal
title="개선이 필요한 점"
keywords={allNegativeKeywords}
/>
);
}

function Review({ data }: { data: MyReviewItem }) {
const [expanded, setExpanded] = useState(false);

Expand Down Expand Up @@ -203,7 +220,7 @@ function Review({ data }: { data: MyReviewItem }) {
<div className="text-text-subtle">
<span className="font-designer-14b mr-100">스터디 주제</span>
<span className="font-designer-13r">
{data.studySubjects.join(', ')}
{data.studySubjects.filter((subject) => subject).join(', ')}
</span>
</div>
</div>
Expand Down
44 changes: 44 additions & 0 deletions src/entities/user/ui/more-keyword-review-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { XIcon } from 'lucide-react';
import { Modal } from '@/shared/ui/modal';
import KeywordReview from './keyword-review';

export default function MoreKeywordReviewModal({
title,
keywords,
}: {
title: string;
keywords: { id: number; content: string; count: number }[];
}) {
return (
<Modal.Root>
<Modal.Trigger asChild>
{keywords.length > 5 && (
<button className="font-designer-12m text-text-subtlest cursor-pointer">
더보기
</button>
)}
</Modal.Trigger>
<Modal.Portal>
<Modal.Overlay />
<Modal.Content size="large" className="w-full">
<Modal.Header className="border-border-default flex justify-between border-b">
<Modal.Title className="font-designer-20b text-text-strong">
{title}
</Modal.Title>
<Modal.Close>
<XIcon />
</Modal.Close>
</Modal.Header>

<Modal.Body className="flex flex-col gap-400 p-400">
<ul className="flex flex-col gap-50">
{keywords.map((keyword) => (
<KeywordReview key={keyword.id} {...keyword} />
))}
</ul>
</Modal.Body>
</Modal.Content>
</Modal.Portal>
</Modal.Root>
);
}
87 changes: 50 additions & 37 deletions src/entities/user/ui/my-profile-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import Link from 'next/link';
import React, { useState } from 'react';
import { usePatchAutoMatchingMutation } from '@/entities/user/model/use-user-profile-query';
import { useReviewReminder } from '@/features/study/lib/use-reminder-review';
import StudyReviewModal from '@/features/study/ui/study-review-modal';
import UserAvatar from '@/shared/ui/avatar';
import { ToggleSwitch } from '@/shared/ui/toggle';
import AccessTimeIcon from 'public/icons/access_time.svg';
Expand Down Expand Up @@ -31,6 +33,8 @@ export default function MyProfileCard({
techStacks,
studyApplied,
}: MyProfileCardProps) {
const { showReviewReminder, setShowReviewReminder } = useReviewReminder();

const [enabled, setEnabled] = useState(matching);

const { mutate: patchAutoMatching, isPending } =
Expand All @@ -52,45 +56,54 @@ export default function MyProfileCard({
};

return (
<section className="rounded-200 border-border-subtle bg-text-inverse flex flex-col items-start gap-200 border p-200">
<div className="flex flex-row items-center gap-200">
<div className="relative h-[64px] w-[64px] shrink-0">
<UserAvatar size={64} image={imageUrl?.trim() || ''} />
<Link
href="my-page"
className="bg-background-accent-gray-strong absolute right-0 bottom-0 flex h-[24px] w-[24px] items-center justify-center rounded-full"
>
<SettingIcon />
</Link>
</div>
<div className="flex flex-col">
<div className="font-designer-18b">{name?.trim() || '비회원'}님</div>
<div className="flex flex-row items-center gap-100">
<span className="font-designer-14r text-gray-800">스터디 매칭</span>
<ToggleSwitch.Root
size="md"
checked={enabled}
onCheckedChange={handleToggleChange}
disabled={isPending || !studyApplied}
/>
<>
<StudyReviewModal
open={showReviewReminder}
onOpenChange={setShowReviewReminder}
/>
<section className="rounded-200 border-border-subtle bg-text-inverse flex flex-col items-start gap-200 border p-200">
<div className="flex flex-row items-center gap-200">
<div className="relative h-[64px] w-[64px] shrink-0">
<UserAvatar size={64} image={imageUrl?.trim() || ''} />
<Link
href="my-page"
className="bg-background-accent-gray-strong absolute right-0 bottom-0 flex h-[24px] w-[24px] items-center justify-center rounded-full"
>
<SettingIcon />
</Link>
</div>
<div className="flex flex-col">
<div className="font-designer-18b">
{name?.trim() || '비회원'}님
</div>
<div className="flex flex-row items-center gap-100">
<span className="font-designer-14r text-gray-800">
스터디 매칭
</span>
<ToggleSwitch.Root
size="md"
checked={enabled}
onCheckedChange={handleToggleChange}
disabled={isPending || !studyApplied}
/>
</div>
</div>
</div>
</div>

<div className="bg-background-alternative rounded-100 font-designer-15m text-text-default flex w-full flex-col gap-200 px-200 py-150">
<div className="flex items-center gap-100">
<AssignmentIcon />
<span>{subject?.trim() || '없음'}</span>
</div>
<div className="flex items-center gap-100">
<AccessTimeIcon />
<span>{time?.trim() || '없음'}</span>
</div>
<div className="flex items-center gap-100">
<CodeIcon />
<span>{techStacks?.trim() || '없음'}</span>
<div className="bg-background-alternative rounded-100 font-designer-15m text-text-default flex w-full flex-col gap-200 px-200 py-150">
<div className="flex items-center gap-100">
<AssignmentIcon />
<span>{subject?.trim() || '없음'}</span>
</div>
<div className="flex items-center gap-100">
<AccessTimeIcon />
<span>{time?.trim() || '없음'}</span>
</div>
<div className="flex items-center gap-100">
<CodeIcon />
<span>{techStacks?.trim() || '없음'}</span>
</div>
</div>
</div>
</section>
</section>
</>
);
}
6 changes: 3 additions & 3 deletions src/entities/user/ui/user-profile-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,14 @@ export default function UserProfileModal({
/>
</div>

<div className="bg-border-subtle h-[2px] w-full" />
<div className="bg-border-subtle h-[2px] w-full flex-none" />

<div className="flex gap-400 px-250">
<div className="flex gap-400 pl-250">
<span className="font-designer-16b text-text-default w-[132px] shrink-0">
받은 평가
</span>

<div className="text-text-default font-designer-14r">
<div className="text-text-default font-designer-14r grow-1">
{/* todo: 기획 fix되면 수정 */}
{/* <span>n명의 유저들이 이런 점이 좋다고 했어요.</span> */}

Expand Down
8 changes: 8 additions & 0 deletions src/features/study/api/get-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
MyNegativeKeywordsResponse,
MyReviewsResponse,
MyReviewsRequest,
ShouldReviewPartnerResponse,
} from './types';

export const getPartnerStudyReview =
Expand Down Expand Up @@ -79,3 +80,10 @@ export const getMyReviews = async ({

return res.data.content;
};

export const getShouldReviewPartner =
async (): Promise<ShouldReviewPartnerResponse> => {
const res = await axiosInstance.get('/study/reviews/this-week/is-writer');

return res.data.content;
};
2 changes: 2 additions & 0 deletions src/features/study/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,5 @@ export interface MyReviewsResponse {
hasNext: boolean;
};
}

export type ShouldReviewPartnerResponse = boolean;
38 changes: 38 additions & 0 deletions src/features/study/lib/use-reminder-review.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';

import { getKoreaDate } from '@/shared/lib/time';
import { useShouldReviewPartnerQuery } from '../model/use-review-query';

export const useReviewReminder = () => {
const { data: shouldReview, isFetching } = useShouldReviewPartnerQuery();
const [showReviewReminder, setShowReviewReminder] = useState(false);

useEffect(() => {
// 이미 리뷰를 달았을 경우
if (!shouldReview || isFetching) return;

const now = getKoreaDate();
const dayOfWeek = now.getDay();

const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; // 일요일(0), 토요일(6)

// 평일인 경우
if (!isWeekend) return;

const lastShown = localStorage.getItem('lastReviewModalShown');

const diff = now.getTime() - Number(lastShown);
const THIRTY_MIN = 1000 * 60 * 30; // 30분

if (!lastShown || diff >= THIRTY_MIN) {
setShowReviewReminder(true);

localStorage.setItem('lastReviewModalShown', String(now.getTime()));
}
}, [shouldReview, isFetching]);

return {
showReviewReminder,
setShowReviewReminder,
};
};
17 changes: 17 additions & 0 deletions src/features/study/model/use-review-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {
useQuery,
useSuspenseQuery,
} from '@tanstack/react-query';
import { getKoreaDate } from '@/shared/lib/time';
import {
addStudyReview,
getUserPositiveKeywords,
getPartnerStudyReview,
getMyNegativeKeywords,
getMyReviews,
getShouldReviewPartner,
} from '../api/get-review';
import {
MyNegativeKeywordsRequest,
Expand Down Expand Up @@ -84,3 +86,18 @@ export const useMyReviewsInfinityQuery = () => {
},
});
};

export const useShouldReviewPartnerQuery = () => {
return useQuery({
queryKey: ['shouldReviewPartner'],
queryFn: getShouldReviewPartner,
refetchInterval: 1000 * 60 * 30, // 30분
enabled: () => {
const now = getKoreaDate();
const dayOfWeek = now.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; // 0: 일요일, 6: 토요일

return isWeekend;
},
});
};
Loading