스터디 기간
@@ -203,7 +237,7 @@ function Review({ data }: { data: MyReviewItem }) {
스터디 주제
- {data.studySubjects.join(', ')}
+ {data.studySubjects.filter((subject) => subject).join(', ')}
diff --git a/next.config.ts b/next.config.ts
index 108ea078..e40cb1e2 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -12,18 +12,18 @@ const nextConfig: NextConfig = {
},
{
protocol: 'https',
- hostname: 'test-api.zeroone.it.kr',
- pathname: '/profile-image/**',
+ hostname: 'lh3.googleusercontent.com',
+ pathname: '/**', // 구글 이미지 전체 허용
},
{
protocol: 'https',
- hostname: 'api.zeroone.it.kr',
- pathname: '/profile-image/**',
+ hostname: 'test-api.zeroone.it.kr',
+ pathname: '/**',
},
{
protocol: 'https',
- hostname: 'lh3.googleusercontent.com',
- pathname: '/**', // 구글 이미지 전체 허용
+ hostname: 'www.zeroone.it.kr',
+ pathname: '/**',
},
],
},
diff --git a/package.json b/package.json
index 4334a658..83f1931e 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-dialog": "^1.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-toggle": "^1.1.9",
diff --git a/public/images/help_outline.png b/public/images/help_outline.png
new file mode 100644
index 00000000..20462273
Binary files /dev/null and b/public/images/help_outline.png differ
diff --git a/src/entities/user/api/types.ts b/src/entities/user/api/types.ts
index fbd0d905..cb5663ea 100644
--- a/src/entities/user/api/types.ts
+++ b/src/entities/user/api/types.ts
@@ -65,19 +65,26 @@ export interface MemberProfile {
simpleIntroduction: string;
mbti: string;
interests: Interest[];
- hobbies: Hobby[];
+ hobbies?: Hobby[];
birthDate: string;
githubLink: SocialLink | undefined;
blogOrSnsLink: SocialLink | undefined;
tel: string;
}
+export interface SincerityTemp {
+ temperature: number;
+ levelId: number;
+ levelName: string;
+}
+
export interface GetUserProfileResponse {
memberId: number;
autoMatching: boolean;
studyApplied: boolean;
memberInfo: MemberInfo;
memberProfile: MemberProfile;
+ sincerityTemp: SincerityTemp;
}
export interface PatchAutoMatchingParams {
diff --git a/src/entities/user/ui/more-keyword-review-modal.tsx b/src/entities/user/ui/more-keyword-review-modal.tsx
new file mode 100644
index 00000000..b55fb84a
--- /dev/null
+++ b/src/entities/user/ui/more-keyword-review-modal.tsx
@@ -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 (
+
+
+ {keywords.length > 5 && (
+
+ )}
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ {keywords.map((keyword) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/entities/user/ui/my-profile-card.tsx b/src/entities/user/ui/my-profile-card.tsx
index 32ab753a..cd2eac19 100644
--- a/src/entities/user/ui/my-profile-card.tsx
+++ b/src/entities/user/ui/my-profile-card.tsx
@@ -3,12 +3,17 @@
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 { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets';
+import { cn } from '@/shared/shadcn/lib/utils';
import UserAvatar from '@/shared/ui/avatar';
import { ToggleSwitch } from '@/shared/ui/toggle';
import AccessTimeIcon from 'public/icons/access_time.svg';
import AssignmentIcon from 'public/icons/assignment.svg';
import CodeIcon from 'public/icons/code.svg';
import SettingIcon from 'public/icons/setting.svg';
+import { SincerityTemp } from '../api/types';
interface MyProfileCardProps {
memberId: number;
@@ -19,6 +24,7 @@ interface MyProfileCardProps {
time?: string;
techStacks?: string;
studyApplied?: boolean;
+ sincerityTemp: SincerityTemp;
}
export default function MyProfileCard({
@@ -30,8 +36,12 @@ export default function MyProfileCard({
time,
techStacks,
studyApplied,
+ sincerityTemp,
}: MyProfileCardProps) {
+ const { showReviewReminder, setShowReviewReminder } = useReviewReminder();
+
const [enabled, setEnabled] = useState(matching);
+ const temperPreset = getSincerityPresetByLevelName(sincerityTemp.levelName);
const { mutate: patchAutoMatching, isPending } =
usePatchAutoMatchingMutation();
@@ -52,45 +62,65 @@ export default function MyProfileCard({
};
return (
-
-
-
-
-
-
-
-
-
-
{name?.trim() || '비회원'}님
-
-
스터디 매칭
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {name?.trim() || '비회원'}님
+
+
+ {sincerityTemp.temperature.toFixed(1)} ℃
+
+
+
+
+ 스터디 매칭
+
+
+
-
-
-
-
-
-
{subject?.trim() || '없음'}
-
-
-
-
{time?.trim() || '없음'}
-
-
-
-
{techStacks?.trim() || '없음'}
+
+
+
+
{subject?.trim() || '없음'}
+
+
+
+
{time?.trim() || '없음'}
+
+
+
+ {techStacks?.trim() || '없음'}
+
-
-
+
+ >
);
}
diff --git a/src/entities/user/ui/user-profile-modal.tsx b/src/entities/user/ui/user-profile-modal.tsx
index 2d98c663..a9ab7436 100644
--- a/src/entities/user/ui/user-profile-modal.tsx
+++ b/src/entities/user/ui/user-profile-modal.tsx
@@ -9,6 +9,7 @@ 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 { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets';
import UserAvatar from '@/shared/ui/avatar';
import Badge from '@/shared/ui/badge';
import { Modal } from '@/shared/ui/modal';
@@ -30,6 +31,9 @@ export default function UserProfileModal({
if (isLoading || isError || !profile || !positiveKeywordsData) return null;
const positiveKeywords = positiveKeywordsData?.keywords || [];
+ const temperPreset = getSincerityPresetByLevelName(
+ profile.sincerityTemp.levelName,
+ );
return (
@@ -68,33 +72,55 @@ export default function UserProfileModal({
))}
-
- {profile.memberProfile.memberName}
+
+
+ {profile.memberProfile.memberName}
+
+
+
+
+
+
+
+ {profile.sincerityTemp.temperature.toFixed(1)} ℃
+
+
{profile.memberProfile.simpleIntroduction}
-
-
-
-
+
+
+
+
+
{profile.memberProfile.birthDate ?? ''}
-
-
-
- {profile.memberProfile.tel ?? ''}
-
+
-
-
-
+
+
+
{profile.memberProfile.githubLink?.url ?? ''}
-
-
-
+
+
+
+
+
+ {profile.memberProfile.tel ?? ''}
+
+
+
+
+
+
{profile.memberProfile.blogOrSnsLink?.url ?? ''}
-
+
@@ -127,14 +153,14 @@ export default function UserProfileModal({
/>
-
+
-
+
받은 평가
-
+
{/* todo: 기획 fix되면 수정 */}
{/*
n명의 유저들이 이런 점이 좋다고 했어요. */}
diff --git a/src/features/my-page/ui/profile.tsx b/src/features/my-page/ui/profile.tsx
index aa685de4..f1db1e9e 100644
--- a/src/features/my-page/ui/profile.tsx
+++ b/src/features/my-page/ui/profile.tsx
@@ -1,18 +1,29 @@
-import { MemberProfile } from '@/entities/user/api/types';
+import Image from 'next/image';
+import { MemberProfile, SincerityTemp } from '@/entities/user/api/types';
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 ProfileEditModal from '@/features/my-page/ui/profile-edit-modal';
+import { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets';
+import { cn } from '@/shared/shadcn/lib/utils';
import UserAvatar from '@/shared/ui/avatar';
import Badge from '@/shared/ui/badge';
+import Progress from '@/shared/ui/progress';
interface ProfileProps {
memberId: number;
memberProfile: MemberProfile;
+ sincerityTemp: SincerityTemp;
}
-export default function Profile({ memberId, memberProfile }: ProfileProps) {
+export default function Profile({
+ memberId,
+ memberProfile,
+ sincerityTemp,
+}: ProfileProps) {
+ const temperPreset = getSincerityPresetByLevelName(sincerityTemp.levelName);
+
return (
-
-
-
-
+
+
+
+
{memberProfile.birthDate ?? '생일을 입력해주세요!'}
-
-
+
+
+
+ {memberProfile.githubLink?.url ?? '깃허브 링크를 입력해주세요!'}
+
+
+
+
+
{memberProfile.tel ?? '번호를 입력해주세요!'}
-
+
+
+
+
+ {memberProfile.blogOrSnsLink?.url ??
+ '블로그 링크를 입력해주세요!'}
+
+
+
-
-
-
- {memberProfile.githubLink?.url || '깃허브 링크를 입력해주세요!'}
+
+
+
+ 성실 온도
+
-
-
- {memberProfile.blogOrSnsLink?.url ||
- '블로그 링크를 입력해주세요!'}
+
+
+
{sincerityTemp.temperature.toFixed(1)} ℃
+
diff --git a/src/features/study/api/get-review.ts b/src/features/study/api/get-review.ts
index e188d28d..58b3a4cb 100644
--- a/src/features/study/api/get-review.ts
+++ b/src/features/study/api/get-review.ts
@@ -8,6 +8,7 @@ import type {
MyNegativeKeywordsResponse,
MyReviewsResponse,
MyReviewsRequest,
+ ShouldReviewPartnerResponse,
} from './types';
export const getPartnerStudyReview =
@@ -79,3 +80,10 @@ export const getMyReviews = async ({
return res.data.content;
};
+
+export const getShouldReviewPartner =
+ async (): Promise
=> {
+ const res = await axiosInstance.get('/study/reviews/this-week/is-writer');
+
+ return res.data.content;
+ };
diff --git a/src/features/study/api/types.ts b/src/features/study/api/types.ts
index 77cfa766..a77cbd89 100644
--- a/src/features/study/api/types.ts
+++ b/src/features/study/api/types.ts
@@ -124,7 +124,7 @@ export interface AddStudyReviewRequest {
targetMemberId: number;
satisfactionId: 10 | 20 | 30;
keywordIds: number[];
- content: string;
+ content?: string;
}
interface Keyword {
@@ -183,3 +183,5 @@ export interface MyReviewsResponse {
hasNext: boolean;
};
}
+
+export type ShouldReviewPartnerResponse = boolean;
diff --git a/src/features/study/lib/use-reminder-review.tsx b/src/features/study/lib/use-reminder-review.tsx
new file mode 100644
index 00000000..4ab66b54
--- /dev/null
+++ b/src/features/study/lib/use-reminder-review.tsx
@@ -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,
+ };
+};
diff --git a/src/features/study/model/use-review-query.ts b/src/features/study/model/use-review-query.ts
index 40e17990..fe2fbfc7 100644
--- a/src/features/study/model/use-review-query.ts
+++ b/src/features/study/model/use-review-query.ts
@@ -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,
@@ -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;
+ },
+ });
+};
diff --git a/src/features/study/ui/study-review-modal.tsx b/src/features/study/ui/study-review-modal.tsx
index fe291dda..e39d325b 100644
--- a/src/features/study/ui/study-review-modal.tsx
+++ b/src/features/study/ui/study-review-modal.tsx
@@ -3,6 +3,7 @@
import { XIcon } from 'lucide-react';
import Image from 'next/image';
import { useState } from 'react';
+import UserAvatar from '@/shared/ui/avatar';
import Button from '@/shared/ui/button';
import Checkbox from '@/shared/ui/checkbox';
import { TextAreaInput } from '@/shared/ui/input';
@@ -22,26 +23,20 @@ interface FormState {
content: string;
}
-export default function StudyReviewModal() {
+export default function StudyReviewModal({
+ open,
+ onOpenChange,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
return (
-
-
-
-
-
- CS 스터디를 시작해 보세요!
-
-
- 스터디 신청하기
-
-
-
-
+
-
+ onOpenChange(false)}>
@@ -57,14 +52,14 @@ export default function StudyReviewModal() {
-
+
onOpenChange(false)} />
);
}
-function StudyReviewForm() {
+function StudyReviewForm({ onClose }: { onClose: () => void }) {
const { data } = usePartnerStudyReviewQuery();
const { mutate: addStudyReview } = useAddStudyReviewMutation();
@@ -79,14 +74,19 @@ function StudyReviewForm() {
if (!data) return null;
const handleSubmit = () => {
- if (
- form.keywordIds.length === 0 ||
- form.satisfactionId === null ||
- form.content === ''
- )
- return;
-
- addStudyReview(form);
+ if (form.keywordIds.length === 0 || form.satisfactionId === null) return;
+
+ addStudyReview(
+ {
+ ...form,
+ content: form.content || undefined,
+ },
+ {
+ onSuccess: () => {
+ onClose();
+ },
+ },
+ );
};
return (
@@ -154,7 +154,7 @@ function StudyReviewForm() {
-
@@ -178,12 +178,10 @@ function PartnerInfo(data: StudyEvaluationResponse) {
return (
-
diff --git a/src/shared/config/sincerity-temp-presets.tsx b/src/shared/config/sincerity-temp-presets.tsx
new file mode 100644
index 00000000..a915d0c3
--- /dev/null
+++ b/src/shared/config/sincerity-temp-presets.tsx
@@ -0,0 +1,63 @@
+import TempType1 from '@/shared/icons/temp_1.svg';
+import TempType2 from '@/shared/icons/temp_2.svg';
+import TempType3 from '@/shared/icons/temp_3.svg';
+import TempType4 from '@/shared/icons/temp_4.svg';
+
+export type SincerityLabel = '1단계' | '2단계' | '3단계' | '4단계';
+
+export interface SincerityPreset {
+ indicatorClass: string;
+ textClass: string;
+ bgClass: string;
+ Icon: React.ComponentType
>;
+ label?: string;
+}
+
+export const SINCERITY_TEMP_PRESETS: Record = {
+ '1단계': {
+ // todo: Figma에 헥스코드로만 존재. 디자인 시스템 X. 현재 코드에서는 헥스값 인식을 하지 못하기 때문에 임의 컬러로 설정
+ // 현재 기획 여쭤본 상태, 컬러값에 따라 global에 추가후 변경 가능성 O.
+ // indicatorClass: '#F5C400',
+ // textClass: '#FFD21F',
+ indicatorClass: 'bg-yellow-300',
+ textClass: 'text-yellow-400',
+ bgClass: 'bg-yellow-50',
+ Icon: TempType1,
+ label: '노란불씨',
+ },
+ '2단계': {
+ indicatorClass: 'bg-orange-400',
+ textClass: 'text-orange-400',
+ bgClass: 'bg-orange-50',
+ Icon: TempType2,
+ label: '주황불꽃',
+ },
+ '3단계': {
+ indicatorClass: 'bg-rose-500',
+ textClass: 'text-rose-500',
+ bgClass: 'bg-rose-50',
+ Icon: TempType3,
+ label: '불꽃',
+ },
+ '4단계': {
+ indicatorClass: 'bg-indigo-500',
+ textClass: 'text-indigo-500',
+ bgClass: 'bg-indigo-50',
+ Icon: TempType4,
+ label: '푸른불꽃',
+ },
+} as const;
+
+export function toLevelLabel(levelName?: string): SincerityLabel {
+ const n = Number((levelName ?? '').match(/\d+/)?.[0] ?? 1);
+ const clamped = Math.min(4, Math.max(1, n || 1));
+
+ return `${clamped}단계` as SincerityLabel;
+}
+
+// 매핑 안되는 값이 들어왔을 경우 FALLBACK
+export function getSincerityPresetByLevelName(
+ levelName?: string,
+): SincerityPreset {
+ return SINCERITY_TEMP_PRESETS[toLevelLabel(levelName)];
+}
diff --git a/src/shared/icons/temp_1.svg b/src/shared/icons/temp_1.svg
new file mode 100644
index 00000000..edbc9675
--- /dev/null
+++ b/src/shared/icons/temp_1.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/icons/temp_2.svg b/src/shared/icons/temp_2.svg
new file mode 100644
index 00000000..3bb87659
--- /dev/null
+++ b/src/shared/icons/temp_2.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/icons/temp_3.svg b/src/shared/icons/temp_3.svg
new file mode 100644
index 00000000..8ca85a33
--- /dev/null
+++ b/src/shared/icons/temp_3.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/icons/temp_4.svg b/src/shared/icons/temp_4.svg
new file mode 100644
index 00000000..60119ca5
--- /dev/null
+++ b/src/shared/icons/temp_4.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/ui/progress/index.tsx b/src/shared/ui/progress/index.tsx
new file mode 100644
index 00000000..fb9ca504
--- /dev/null
+++ b/src/shared/ui/progress/index.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import * as RadixProgress from '@radix-ui/react-progress';
+import * as React from 'react';
+
+interface ProgressProps {
+ value: number;
+ indicatorColor?: string;
+}
+
+const Progress: React.FC = ({
+ value,
+ indicatorColor = 'bg-white',
+}) => {
+ return (
+
+
+
+ );
+};
+
+export default Progress;
diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx
index f588e6cb..d6713d28 100644
--- a/src/widgets/home/sidebar.tsx
+++ b/src/widgets/home/sidebar.tsx
@@ -30,6 +30,7 @@ export default async function Sidebar() {
?.map((t) => t.techStackName)
.join(', ')}
studyApplied={userProfile?.studyApplied ?? false}
+ sincerityTemp={userProfile.sincerityTemp}
/>
{userProfile.studyApplied ? (
@@ -37,12 +38,23 @@ export default async function Sidebar() {
+
+
+
+ CS 스터디를 시작해 보세요!
+
+
+ 스터디 신청하기
+
+
+
+
+
}
/>
)}
diff --git a/yarn.lock b/yarn.lock
index 15d0327d..2f43b386 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2926,6 +2926,26 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-progress@npm:^1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-progress@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/bed5349682a75db02d362c07ac99fefddbbdc0152c4d5035719498223b9d490ebd834e2d9f64d498424048eb3da7eb7e55ba696e202cd0a048d6e319390e69d3
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-roving-focus@npm:1.1.10":
version: 1.1.10
resolution: "@radix-ui/react-roving-focus@npm:1.1.10"
@@ -10605,6 +10625,7 @@ __metadata:
"@radix-ui/react-avatar": "npm:^1.1.9"
"@radix-ui/react-dialog": "npm:^1.1.10"
"@radix-ui/react-dropdown-menu": "npm:^2.1.15"
+ "@radix-ui/react-progress": "npm:^1.1.7"
"@radix-ui/react-slot": "npm:^1.2.2"
"@radix-ui/react-switch": "npm:^1.2.4"
"@radix-ui/react-toggle": "npm:^1.1.9"