diff --git a/app/(my)/my-study-review/page.tsx b/app/(my)/my-study-review/page.tsx new file mode 100644 index 00000000..730daa9d --- /dev/null +++ b/app/(my)/my-study-review/page.tsx @@ -0,0 +1,212 @@ +'use client'; + +import Image from 'next/image'; +import { useState } from 'react'; +import KeywordReview from '@/entities/user/ui/keyword-review'; +import { MyReviewItem } from '@/features/study/api/types'; +import { + useMyNegativeKeywordsQuery, + useMyReviewsInfinityQuery, + useUserPositiveKeywordsQuery, +} from '@/features/study/model/use-review-query'; +import { formatKoreaRelativeTime } from '@/shared/lib/time'; + +export default function MyStudyReview() { + const { data: positiveKeywordsData } = useUserPositiveKeywordsQuery({ + pageSize: 5, + }); + const { data: negativeKeywordsData } = useMyNegativeKeywordsQuery({ + pageSize: 5, + }); + const { + data: myReviewsData, + fetchNextPage, + hasNextPage, + } = useMyReviewsInfinityQuery(); + + const positiveKeywords = positiveKeywordsData?.keywords || []; + const negativeKeywords = negativeKeywordsData?.keywords || []; + + const positiveKeywordsCount = positiveKeywordsData?.totalCount || 0; + const negativeKeywordsCount = negativeKeywordsData?.totalCount || 0; + + const totalKeywordsCount = positiveKeywordsCount + negativeKeywordsCount; + + const myReviews = myReviewsData?.reviews || []; + + return ( + <> +
+
+
+
받은 평가
+
+ {totalKeywordsCount} +
+
+ + + 개선이 필요한 점은 나에게만 보여요 + +
+ +
+
+
+

좋았던 점

+ + {positiveKeywords.length > 5 && ( + + )} +
+ +
    + {positiveKeywords.length > 0 ? ( + positiveKeywords.map((keyword) => ( + + )) + ) : ( + + 아직 받은 평가가 없습니다. + + )} +
+
+ +
+
+

+ 개선이 필요한 점 +

+ + {negativeKeywords.length > 5 && ( + + )} +
+ +
    + {negativeKeywords.length > 0 ? ( + negativeKeywords.map((keyword) => ( + + )) + ) : ( + + 아직 받은 평가가 없습니다. + + )} +
+
+
+
+ +
+
+
후기
+
+ {myReviewsData?.totalCount || 0} +
+
+ + + 모든 후기는 나에게만 보여요 + + + +
+ + ); +} + +function Review({ data }: { data: MyReviewItem }) { + const [expanded, setExpanded] = useState(false); + + return ( +
  • +
    + {`${data.writer.memberName} + +
    + + {data.writer.memberName} + + · + + {formatKoreaRelativeTime(data.reviewedAt)} + +
    +
    + +
    +

    + {data.content} +

    + +
    + +
    +
    + 스터디 기간 + + {data.startDate.replace(/-/g, '.')} ~{' '} + {data.endDate.replace(/-/g, '.')} + +
    +
    + 스터디 주제 + + {data.studySubjects.join(', ')} + +
    +
    +
  • + ); +} diff --git a/app/global.css b/app/global.css index 74119091..1cb1b7fa 100644 --- a/app/global.css +++ b/app/global.css @@ -192,6 +192,10 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E --color-background-brand-default: var(--color-rose-500); --color-background-brand-strong: var(--color-rose-700); + --color-background-neutral-subtle: var(--color-gray-200); + --color-background-neutral-default: var(--color-gray-500); + --color-background-neutral-strong: var(--color-gray-900); + --color-background-accent-blue-subtle: var(--color-blue-50); --color-background-accent-blue-default: var(--color-blue-100); --color-background-accent-blue-strong: var(--color-blue-600); @@ -226,6 +230,10 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E --color-background-accent-yellow-default: var(--color-yellow-100); --color-background-accent-yellow-strong: var(--color-yellow-600); + --color-background-success-subtle: var(--color-green-300); + --color-background-success-default: var(--color-green-500); + --color-background-success-strong: var(--color-green-700); + --color-fill-brand-default-default: var(--color-rose-500); --color-fill-brand-default-hover: var(--color-rose-600); --color-fill-brand-default-pressed: var(--color-rose-700); diff --git a/next.config.ts b/next.config.ts index e1dcedae..108ea078 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,6 +15,11 @@ const nextConfig: NextConfig = { hostname: 'test-api.zeroone.it.kr', pathname: '/profile-image/**', }, + { + protocol: 'https', + hostname: 'api.zeroone.it.kr', + pathname: '/profile-image/**', + }, { protocol: 'https', hostname: 'lh3.googleusercontent.com', diff --git a/public/apply-study.svg b/public/apply-study.svg new file mode 100644 index 00000000..36d1f6ac --- /dev/null +++ b/public/apply-study.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/feedback.svg b/public/feedback.svg new file mode 100644 index 00000000..60d352ad --- /dev/null +++ b/public/feedback.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/arrow-down.svg b/public/icons/arrow-down.svg new file mode 100644 index 00000000..10f6a659 --- /dev/null +++ b/public/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/fine-review.svg b/public/icons/fine-review.svg new file mode 100644 index 00000000..13307185 --- /dev/null +++ b/public/icons/fine-review.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/good-review.svg b/public/icons/good-review.svg new file mode 100644 index 00000000..e4b9aeeb --- /dev/null +++ b/public/icons/good-review.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/shame-review.svg b/public/icons/shame-review.svg new file mode 100644 index 00000000..50ee650e --- /dev/null +++ b/public/icons/shame-review.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/shape.svg b/public/icons/shape.svg new file mode 100644 index 00000000..af8ad1d5 --- /dev/null +++ b/public/icons/shape.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/start-study.png b/public/images/start-study.png deleted file mode 100644 index 52b6682a..00000000 Binary files a/public/images/start-study.png and /dev/null differ diff --git a/src/entities/user/ui/keyword-review.tsx b/src/entities/user/ui/keyword-review.tsx new file mode 100644 index 00000000..89ecda2a --- /dev/null +++ b/src/entities/user/ui/keyword-review.tsx @@ -0,0 +1,14 @@ +export default function KeywordReview({ + content, + count, +}: { + content: string; + count: number; +}) { + return ( +
  • + {content} + {count} +
  • + ); +} diff --git a/src/widgets/my-page/profileinfo-card.tsx b/src/entities/user/ui/profile-info-card.tsx similarity index 82% rename from src/widgets/my-page/profileinfo-card.tsx rename to src/entities/user/ui/profile-info-card.tsx index fccee5b8..4e5e1db9 100644 --- a/src/widgets/my-page/profileinfo-card.tsx +++ b/src/entities/user/ui/profile-info-card.tsx @@ -6,7 +6,7 @@ interface Props { export default function ProfileInfoCard({ title, content }: Props) { return (
    -
    +
    {title}
    diff --git a/src/features/my-page/ui/profile-info.tsx b/src/features/my-page/ui/profile-info.tsx index 93e87afe..9b0047cc 100644 --- a/src/features/my-page/ui/profile-info.tsx +++ b/src/features/my-page/ui/profile-info.tsx @@ -1,8 +1,8 @@ 'use client'; import { MemberInfo } from '@/entities/user/api/types'; +import ProfileInfoCard from '@/entities/user/ui/profile-info-card'; import ProfileInfoEditModal from '@/features/my-page/ui/profile-info-edit-modal'; -import ProfileInfoCard from '@/widgets/my-page/profileinfo-card'; interface ProfileInfoProps { memberId: number; diff --git a/src/features/my-page/ui/user-profile-modal.tsx b/src/features/my-page/ui/user-profile-modal.tsx index 0cd0f824..2d98c663 100644 --- a/src/features/my-page/ui/user-profile-modal.tsx +++ b/src/features/my-page/ui/user-profile-modal.tsx @@ -2,14 +2,16 @@ import { XIcon } from 'lucide-react'; import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query'; +import KeywordReview from '@/entities/user/ui/keyword-review'; +import ProfileInfoCard from '@/entities/user/ui/profile-info-card'; import CakeIcon from '@/features/my-page/ui/icon/cake.svg'; import GithubIcon from '@/features/my-page/ui/icon/github-logo.svg'; import GlobeIcon from '@/features/my-page/ui/icon/globe-simple.svg'; import PhoneIcon from '@/features/my-page/ui/icon/phone.svg'; +import { useUserPositiveKeywordsQuery } from '@/features/study/model/use-review-query'; import UserAvatar from '@/shared/ui/avatar'; import Badge from '@/shared/ui/badge'; import { Modal } from '@/shared/ui/modal'; -import ProfileInfoCard from '@/widgets/my-page/profileinfo-card'; interface UserProfileModalProps { memberId: number; @@ -21,8 +23,13 @@ export default function UserProfileModal({ trigger, }: UserProfileModalProps) { const { data: profile, isLoading, isError } = useUserProfileQuery(memberId); + const { data: positiveKeywordsData } = useUserPositiveKeywordsQuery({ + memberId, + }); - if (isLoading || isError || !profile) return null; + if (isLoading || isError || !profile || !positiveKeywordsData) return null; + + const positiveKeywords = positiveKeywordsData?.keywords || []; return ( @@ -71,25 +78,22 @@ export default function UserProfileModal({
    - {profile.memberProfile.birthDate ?? - '생일을 입력해주세요!'} + {profile.memberProfile.birthDate ?? ''}
    - {profile.memberProfile.tel ?? '번호를 입력해주세요!'} + {profile.memberProfile.tel ?? ''}
    - {profile.memberProfile.githubLink?.url ?? - '깃허브 링크를 입력해주세요!'} + {profile.memberProfile.githubLink?.url ?? ''}
    - {profile.memberProfile.blogOrSnsLink?.url ?? - '블로그 링크를 입력해주세요!'} + {profile.memberProfile.blogOrSnsLink?.url ?? ''}
    @@ -122,6 +126,35 @@ export default function UserProfileModal({ content={profile.memberInfo.studyPlan} />
    + +
    + +
    + + 받은 평가 + + +
    + {/* todo: 기획 fix되면 수정 */} + {/* n명의 유저들이 이런 점이 좋다고 했어요. */} + +
      + {positiveKeywords.length > 0 ? ( + positiveKeywords.map((keyword) => ( + + )) + ) : ( + + 아직 받은 평가가 없습니다. + + )} +
    +
    +
    diff --git a/src/features/study/api/get-review.ts b/src/features/study/api/get-review.ts new file mode 100644 index 00000000..e188d28d --- /dev/null +++ b/src/features/study/api/get-review.ts @@ -0,0 +1,81 @@ +import { axiosInstance } from '@/shared/tanstack-query/axios'; +import type { + AddStudyReviewRequest, + UserPositiveKeywordsResponse, + UserPositiveKeywordsRequest, + StudyEvaluationResponse, + MyNegativeKeywordsRequest, + MyNegativeKeywordsResponse, + MyReviewsResponse, + MyReviewsRequest, +} from './types'; + +export const getPartnerStudyReview = + async (): Promise => { + const res = await axiosInstance.get( + '/study/reviews/this-week/target-study', + ); + + return res.data.content; + }; + +export const addStudyReview = async (data: AddStudyReviewRequest) => { + const res = await axiosInstance.post('/study/reviews', data); + + return res.data.content; +}; + +export const getUserPositiveKeywords = async ({ + memberId, + pageSize, +}: UserPositiveKeywordsRequest): Promise => { + const params: Record = {}; + + if (memberId) { + params['member-id'] = memberId; + } + if (pageSize) { + params['page-size'] = pageSize; + } + + const res = await axiosInstance.get( + '/study/reviews/members/keywords/positive', + { params }, + ); + + return res.data.content; +}; + +export const getMyNegativeKeywords = async ({ + pageSize, +}: MyNegativeKeywordsRequest): Promise => { + const params: Record = {}; + + if (pageSize) { + params['page-size'] = pageSize; + } + + const res = await axiosInstance.get( + '/study/reviews/members/keywords/negative', + { params }, + ); + + return res.data.content; +}; + +export const getMyReviews = async ({ + cursor, +}: MyReviewsRequest): Promise => { + const params: Record = { + 'page-size': 10, + }; + + // cursor 전송하지 않는 경우 첫 데이터부터 조회 + if (cursor) { + params.cursor = cursor; + } + + const res = await axiosInstance.get('/study/reviews/members', { params }); + + return res.data.content; +}; diff --git a/src/features/study/api/types.ts b/src/features/study/api/types.ts index f790661b..77cfa766 100644 --- a/src/features/study/api/types.ts +++ b/src/features/study/api/types.ts @@ -94,3 +94,92 @@ export interface CompleteStudyRequest { feedback: string; progressStatus: StudyProgressStatus; } + +export interface EvalKeyword { + id: number; + keyword: string; + satisfactionId: number; + satisfactionLabel: string; +} + +interface Partner { + memberId: number; + memberName: string; + profileImageUrl: string; +} + +export interface StudyEvaluationResponse { + studySpaceId: number; + targetMembers: Partner[]; + studySubject: string; + startDate: string; // "yyyy-MM-dd" 형식 + endDate: string; // "yyyy-MM-dd" 형식 + satisfiedEvalKeywords: EvalKeyword[]; + notBadEvalKeywords: EvalKeyword[]; + unsatisfiedEvalKeywords: EvalKeyword[]; +} + +export interface AddStudyReviewRequest { + studySpaceId: number; + targetMemberId: number; + satisfactionId: 10 | 20 | 30; + keywordIds: number[]; + content: string; +} + +interface Keyword { + id: number; + content: string; + count: number; +} + +export interface UserPositiveKeywordsRequest { + memberId?: number; + pageSize?: number; +} + +export interface UserPositiveKeywordsResponse { + totalCount: number | null; // params에 pageSize 값을 보내지 않는 경우 null + reviewerCount: number | null; // params에 pageSize 값을 보내지 않는 경우 null + keywords: Keyword[]; +} + +export interface MyNegativeKeywordsRequest { + pageSize?: number; +} + +export interface MyNegativeKeywordsResponse { + totalCount: number | null; // params에 pageSize 값을 보내지 않는 경우 null + reviewerCount: number | null; // params에 pageSize 값을 보내지 않는 경우 null + keywords: Keyword[]; +} + +export interface MyReviewWriter { + memberId: number; + memberName: string; + profileImageUrl: string; +} + +export interface MyReviewItem { + id: number; + writer: MyReviewWriter; + reviewedAt: string; // ISO 날짜 문자열 + content: string; + studySpaceId: number; + startDate: string; // YYYY-MM-DD + endDate: string; // YYYY-MM-DD + studySubjects: string[]; +} + +export interface MyReviewsRequest { + cursor: number | null; +} + +export interface MyReviewsResponse { + totalCount: number; + reviews: { + items: MyReviewItem[]; + nextCursor: number; + hasNext: boolean; + }; +} diff --git a/src/features/study/model/use-review-query.ts b/src/features/study/model/use-review-query.ts new file mode 100644 index 00000000..40e17990 --- /dev/null +++ b/src/features/study/model/use-review-query.ts @@ -0,0 +1,86 @@ +import { + useInfiniteQuery, + useMutation, + useQuery, + useSuspenseQuery, +} from '@tanstack/react-query'; +import { + addStudyReview, + getUserPositiveKeywords, + getPartnerStudyReview, + getMyNegativeKeywords, + getMyReviews, +} from '../api/get-review'; +import { + MyNegativeKeywordsRequest, + UserPositiveKeywordsRequest, +} from '../api/types'; + +export const usePartnerStudyReviewQuery = () => { + return useSuspenseQuery({ + queryKey: ['partnerStudyReview'], + queryFn: getPartnerStudyReview, + }); +}; + +export const useAddStudyReviewMutation = () => { + return useMutation({ + mutationFn: addStudyReview, + onSuccess: () => { + // todo: 모달로 변경 + alert('후기 작성이 완료되었습니다.'); + }, + }); +}; + +export const useUserPositiveKeywordsQuery = ( + params: UserPositiveKeywordsRequest, +) => { + return useQuery({ + queryKey: ['userPositiveKeywords', params], + queryFn: ({ queryKey }) => { + const [, requestParams] = queryKey as [ + string, + UserPositiveKeywordsRequest, + ]; + + return getUserPositiveKeywords(requestParams); + }, + }); +}; + +export const useMyNegativeKeywordsQuery = ( + params: MyNegativeKeywordsRequest, +) => { + return useQuery({ + queryKey: ['myNegativeKeywords', params], + queryFn: () => getMyNegativeKeywords(params), + }); +}; + +export const useMyReviewsInfinityQuery = () => { + return useInfiniteQuery({ + queryKey: ['myReviews'], + queryFn: ({ pageParam = null }) => getMyReviews({ cursor: pageParam }), + initialPageParam: null, + getNextPageParam: (lastPage) => { + if (lastPage.reviews.hasNext) { + return lastPage.reviews.nextCursor; + } + + return undefined; + }, + select: (data) => { + const allReviews = data.pages.flatMap((page) => page.reviews.items); + const lastPage = data.pages[data.pages.length - 1]; + const totalCount = data.pages[0].totalCount; + const hasNext = lastPage.reviews.hasNext; + + return { + reviews: allReviews, + totalCount, + hasNext, + }; + }, + }); +}; diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx index 4c3a190e..f563fe57 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/ui/start-study-modal.tsx @@ -89,14 +89,23 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) { return ( - 스터디 시작 버튼 +
    +

    + + CS 스터디를 시작해 보세요! + + + 스터디 신청하기 + +

    + + 스터디 시작 버튼 +
    diff --git a/src/features/study/ui/study-review-modal.tsx b/src/features/study/ui/study-review-modal.tsx new file mode 100644 index 00000000..e2dbd8db --- /dev/null +++ b/src/features/study/ui/study-review-modal.tsx @@ -0,0 +1,437 @@ +'use client'; + +import { XIcon } from 'lucide-react'; +import Image from 'next/image'; +import { useState } from 'react'; +import Button from '@/shared/ui/button'; +import Checkbox from '@/shared/ui/checkbox'; +import { TextAreaInput } from '@/shared/ui/input'; +import ListItem from '@/shared/ui/list-item'; +import { Modal } from '@/shared/ui/modal'; +import { EvalKeyword, StudyEvaluationResponse } from '../api/types'; +import { + useAddStudyReviewMutation, + usePartnerStudyReviewQuery, +} from '../model/use-review-query'; + +interface FormState { + studySpaceId: number; + targetMemberId: number; + satisfactionId: 10 | 20 | 30 | null; // 10 - "아쉬워요", 20 - "괜찮아요", 30 - "좋았어요" + keywordIds: number[]; + content: string; +} + +export default function StudyReviewModal() { + return ( + + +
    +

    + + CS 스터디를 시작해 보세요! + + + 스터디 신청하기 + +

    +
    +
    + + + + + + + + + +
    + 함께 스터디한 멤버에 대해 알려주세요 + +
    + + 같이 성장할 수 있는 스터디 문화를 만들기 위해 평가를 남겨주세요. + + 평가한 내용은 성실 온도에 반영됩니다. +
    +
    + + +
    +
    +
    + ); +} + +function StudyReviewForm() { + const { data } = usePartnerStudyReviewQuery(); + const { mutate: addStudyReview } = useAddStudyReviewMutation(); + + const [form, setForm] = useState({ + studySpaceId: data?.studySpaceId, + targetMemberId: data?.targetMembers[0].memberId, + satisfactionId: null, + keywordIds: [], + content: '', + }); + + if (!data) return null; + + const handleSubmit = () => { + if ( + form.keywordIds.length === 0 || + form.satisfactionId === null || + form.content === '' + ) + return; + + addStudyReview(form); + }; + + return ( + <> + + + +
    + + 스터디 만족도 + + +
    + { + setForm({ + ...form, + satisfactionId: 10, + keywordIds: [], + content: '', + }); + }} + /> + + { + setForm({ + ...form, + satisfactionId: 20, + keywordIds: [], + content: '', + }); + }} + /> + + { + setForm({ + ...form, + satisfactionId: 30, + keywordIds: [], + content: '', + }); + }} + /> +
    +
    + + {form.satisfactionId === 10 && ( + + )} + + {(form.satisfactionId === 20 || form.satisfactionId === 30) && ( + + )} +
    + + + + + + + + ); +} + +function PartnerInfo(data: StudyEvaluationResponse) { + const partner = data.targetMembers[0]; + + return ( +
    + Study Member + +
    + + {partner.memberName} + + +

    + + {data.studySubject} + + + {data.startDate} ~ {data.endDate} + +

    +
    +
    + ); +} + +function SatisfactionButton({ + label, + isSelected, + imageSrc, + onClick, +}: { + label: string; + isSelected: boolean; + imageSrc: string; + onClick: () => void; +}) { + return ( + + ); +} + +function PositiveReview({ + data, + form, + onChange, +}: { + form: FormState; + data: StudyEvaluationResponse; + onChange: (form: FormState | ((prev: FormState) => FormState)) => void; +}) { + return ( + <> + onChange((prev) => ({ ...prev, keywordIds }))} + /> + onChange((prev) => ({ ...prev, content }))} + /> + + ); +} + +function NegativeReview({ + data, + form, + onChange, +}: { + data: StudyEvaluationResponse; + form: FormState; + onChange: (form: FormState | ((prev: FormState) => FormState)) => void; +}) { + return ( + <> + onChange((prev) => ({ ...prev, keywordIds }))} + /> + onChange((prev) => ({ ...prev, content }))} + /> + + ); +} + +function PositiveCheckboxList({ + positiveKeywords, + keywordIds, + onChange, +}: { + positiveKeywords: EvalKeyword[]; + keywordIds: FormState['keywordIds']; + onChange: (keywordIds: FormState['keywordIds']) => void; +}) { + const handleToggle = (id: number) => { + const isChecked = keywordIds.includes(id); + const newKeywordIds = isChecked + ? keywordIds.filter((k) => k !== id) + : [...keywordIds, id]; + + onChange(newKeywordIds); + }; + + return ( +
    +
    + + 이런 점이 좋았어요 + + 필수 +
    + +
      + {positiveKeywords.map(({ id, keyword }) => ( + + { + handleToggle(id); + }} + /> + + + ))} +
    +
    + ); +} + +function PositiveTextArea({ + value, + onChange, +}: { + value: string; + onChange: (content: string) => void; +}) { + return ( +
    +
    +

    + 어떤 점이 좋았나요? +

    +
    + + 같이 성장할 수 있는 스터디 문화를 만들기 위해 평가를 남겨주세요. + + 평가한 내용은 성실 온도에 반영됩니다. +
    +
    + + +
    + ); +} + +function NegativeCheckboxList({ + negativeKeywords, + keywordIds, + onChange, +}: { + negativeKeywords: StudyEvaluationResponse['unsatisfiedEvalKeywords']; + keywordIds: FormState['keywordIds']; + onChange: (keywordIds: FormState['keywordIds']) => void; +}) { + const handleToggle = (id: number) => { + const isChecked = keywordIds.includes(id); + const newKeywordIds = isChecked + ? keywordIds.filter((k) => k !== id) + : [...keywordIds, id]; + + onChange(newKeywordIds); + }; + + return ( +
    +
    + + 이런 점이 아쉬웠어요 + + 필수 +
    + +
      + {negativeKeywords.map(({ id, keyword }) => ( + + { + handleToggle(id); + }} + /> + + + ))} +
    +
    + ); +} + +function NegativeTextArea({ + value, + onChange, +}: { + value: string; + onChange: (content: string) => void; +}) { + return ( +
    +
    +

    + 어떤 점이 아쉬웠나요? +

    +
    + + 스터디 과정에서 아쉬웠던 점이 있다면, 이는 성장을 위한 소중한 + 피드백이 됩니다. + + 작성하신 내용은 오직 상대방만 확인할 수 있어요. +
    +
    + + +
    + ); +} diff --git a/src/shared/lib/time.ts b/src/shared/lib/time.ts new file mode 100644 index 00000000..a4b6bc96 --- /dev/null +++ b/src/shared/lib/time.ts @@ -0,0 +1,37 @@ +import { + differenceInDays, + differenceInHours, + differenceInMinutes, + parseISO, +} from 'date-fns'; +import { format } from 'path'; + +export const getKoreaDate = (targetDate?: Date) => { + const date = targetDate || new Date(); + const utc = date.getTime() + date.getTimezoneOffset() * 60 * 1000; // 1970년 1월 1일로부터 현재까지 지난 시간 (밀리초) + + const koreaTimeDiff = 9 * 60 * 60 * 1000; // 한국은 UTC보다 9시간 빠름 + + const koreaNow = new Date(utc + koreaTimeDiff); + + return koreaNow; +}; + +export const formatKoreaRelativeTime = (targetDateStr: string): string => { + const targetDate = parseISO(targetDateStr); + const koreaTarget = getKoreaDate(targetDate); // 한국 시간 변환 + const koreaNow = getKoreaDate(); + + const minutes = differenceInMinutes(koreaNow, koreaTarget); + + if (minutes < 1) return '지금'; // 1분 미만이면 "지금" + if (minutes < 60) return `${minutes}분 전`; // 60분 미만이면 "n분 전" + + const hours = differenceInHours(koreaNow, koreaTarget); + if (hours < 24) return `${hours}시간 전`; // 24시간 미만이면 "n시간 전" + + const days = differenceInDays(koreaNow, koreaTarget); + if (days < 30) return `${days}일 전`; // 30일 미만이면 "n일 전" + + return targetDateStr; +}; diff --git a/src/shared/shadcn/ui/button.tsx b/src/shared/shadcn/ui/button.tsx index b81e3ffb..d61405e4 100644 --- a/src/shared/shadcn/ui/button.tsx +++ b/src/shared/shadcn/ui/button.tsx @@ -50,7 +50,7 @@ function Button({ return ( ); diff --git a/src/shared/ui/checkbox/index.tsx b/src/shared/ui/checkbox/index.tsx new file mode 100644 index 00000000..6652cf8c --- /dev/null +++ b/src/shared/ui/checkbox/index.tsx @@ -0,0 +1,39 @@ +import Image from 'next/image'; + +const Checkbox = ({ + id, + defaultChecked = false, + checked = false, + onToggle, +}: { + id: string; + defaultChecked?: boolean; + checked?: boolean; + onToggle: () => void; +}) => { + return ( + + ); +}; + +export default Checkbox; diff --git a/src/shared/ui/list-item/index.tsx b/src/shared/ui/list-item/index.tsx new file mode 100644 index 00000000..0c5e8874 --- /dev/null +++ b/src/shared/ui/list-item/index.tsx @@ -0,0 +1,22 @@ +import { cn } from '@/shared/shadcn/lib/utils'; + +const ListItem = ({ + className = '', + children, +}: { + className?: string; + children: React.ReactNode; +}) => { + return ( +
  • + {children} +
  • + ); +}; + +export default ListItem; diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index 9709d142..bd51ad86 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -1,3 +1,5 @@ +import Image from 'next/image'; +import Link from 'next/link'; import { getUserProfile } from '@/entities/user/api/get-user-profile'; import MyProfileCard from '@/features/study/ui/my-profile-card'; import StartStudyModal from '@/features/study/ui/start-study-modal'; @@ -34,6 +36,21 @@ export default async function Sidebar() { ) : ( )} + +

    + + 여러분의 의견이 궁금해요! + + + 소중한 피드백을 기다리고 있어요 + +

    + + 피드백 + ); diff --git a/src/widgets/my-page/sidebar.tsx b/src/widgets/my-page/sidebar.tsx index 2e9d9cb1..af28bc7a 100644 --- a/src/widgets/my-page/sidebar.tsx +++ b/src/widgets/my-page/sidebar.tsx @@ -49,9 +49,15 @@ export default function Sidebar() { > 마이스터디 - {}} isActive={false}> - 계정설정 + router.push('/my-study-review')} + isActive={pathname === '/my-study-review'} + > + 스터디 리뷰 + {/* {}} isActive={false}> + 계정설정 + */}
    로그아웃