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}
+
+
+
+
+ 모든 후기는 나에게만 보여요
+
+
+
+ {myReviews.length > 0 ? (
+ myReviews.map((review) => )
+ ) : (
+
+ 아직까지 받은 후기가 없습니다.
+
+ )}
+
+ {hasNextPage && (
+
+ )}
+
+
+ >
+ );
+}
+
+function Review({ data }: { data: MyReviewItem }) {
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+
+
+
+
+
+ {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 (
+
+
+
+
+
+ {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}>
+ 계정설정
+ */}
로그아웃