From c0a2293f68e85d77beb2e7690b9c40a21719b8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 16:21:40 +0900 Subject: [PATCH 01/12] =?UTF-8?q?fix:=20Data-selector=20=EB=82=A0=EC=A7=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/data-selector.tsx | 11 ++++++----- src/features/study/ui/study-card.tsx | 24 +++++++++++++++--------- src/shared/lib/time.ts | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/features/study/ui/data-selector.tsx b/src/features/study/ui/data-selector.tsx index e1c94d2c..0f75625e 100644 --- a/src/features/study/ui/data-selector.tsx +++ b/src/features/study/ui/data-selector.tsx @@ -1,7 +1,8 @@ 'use client'; -import { startOfWeek, addDays, format, isSameDay } from 'date-fns'; +import { addDays, format, isSameDay } from 'date-fns'; import { useMemo } from 'react'; +import { getKoreaDisplayMonday } from '@/shared/lib/time'; interface Props { value: Date; @@ -9,12 +10,12 @@ interface Props { } export default function DateSelector({ value, onChange }: Props) { - const today = new Date(); - const monday = startOfWeek(today, { weekStartsOn: 1 }); const dayLabels = ['월', '화', '수', '목', '금']; + + const displayMonday = useMemo(() => getKoreaDisplayMonday(), []); const dates = useMemo( - () => Array.from({ length: 5 }, (_, i) => addDays(monday, i)), - [monday], + () => Array.from({ length: 5 }, (_, i) => addDays(displayMonday, i)), + [displayMonday], ); return ( diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/ui/study-card.tsx index 0edb1e8d..04082f97 100644 --- a/src/features/study/ui/study-card.tsx +++ b/src/features/study/ui/study-card.tsx @@ -1,7 +1,12 @@ 'use client'; import { getMonth, getDay, startOfWeek, getDate } from 'date-fns'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; +import { + formatKoreaYMD, + getKoreaDate, + getKoreaDisplayMonday, +} from '@/shared/lib/time'; import DateSelector from './data-selector'; import TodayStudyCard from './today-study-card'; import StudyListSection from '../../../widgets/home/study-list-table'; @@ -12,13 +17,13 @@ import ReservationList from '../participation/ui/reservation-list'; // 스터디 주차 구하는 함수 function getWeekly(date: Date): { month: number; week: number } { const weekStartsOn = 0; - const target = new Date(date); - const currentWeekStart = startOfWeek(target, { weekStartsOn }); + const targetKST = getKoreaDate(date); + const currentWeekStart = startOfWeek(targetKST, { weekStartsOn }); const baseMonth = getMonth(currentWeekStart); // 목요일 기준 월의 첫 주 시작일 계산 - const firstOfMonth = new Date(target.getFullYear(), baseMonth, 1); + const firstOfMonth = new Date(targetKST.getFullYear(), baseMonth, 1); const firstWeekStart = startOfWeek(firstOfMonth, { weekStartsOn }); const firstDayOfWeek = getDay(firstOfMonth); const officialFirstWeekStart = @@ -56,17 +61,18 @@ function getWeekly(date: Date): { month: number; week: number } { export default function StudyCard() { const [selectedDate, setSelectedDate] = useState(new Date()); - const offset = selectedDate.getTimezoneOffset() * 60000; // ms단위라 60000곱해줌 - const dateOffset = new Date(selectedDate.getTime() - offset); - - const studyDate = dateOffset.toISOString().split('T')[0]; + const studyDate = formatKoreaYMD(selectedDate); const { data: status } = useStudyStatusQuery(); const { data: participationData } = useWeeklyParticipation(studyDate); const isParticipate = participationData?.isParticipate ?? false; - const { month, week } = getWeekly(selectedDate); + const displayMonday = useMemo( + () => getKoreaDisplayMonday(selectedDate), + [selectedDate], + ); + const { month, week } = getWeekly(displayMonday); return ( <> diff --git a/src/shared/lib/time.ts b/src/shared/lib/time.ts index 662bba84..9ef0d565 100644 --- a/src/shared/lib/time.ts +++ b/src/shared/lib/time.ts @@ -1,10 +1,13 @@ import { + addDays, differenceInDays, differenceInHours, differenceInMinutes, + format, + getDay, parseISO, + startOfWeek, } from 'date-fns'; -import { format } from 'path'; export const getKoreaDate = (targetDate?: Date) => { const date = targetDate || new Date(); @@ -17,6 +20,9 @@ export const getKoreaDate = (targetDate?: Date) => { return koreaNow; }; +export const formatKoreaYMD = (targetDate?: Date) => + format(getKoreaDate(targetDate), 'yyyy-MM-dd'); + export const formatKoreaRelativeTime = (targetDateStr: string): string => { const targetDate = parseISO(targetDateStr); const koreaTarget = getKoreaDate(targetDate); // 한국 시간 변환 @@ -35,3 +41,11 @@ export const formatKoreaRelativeTime = (targetDateStr: string): string => { return targetDateStr; }; + +export const getKoreaDisplayMonday = (base?: Date) => { + const todayKST = getKoreaDate(base); + const monday = startOfWeek(todayKST, { weekStartsOn: 1 }); // 월요일 시작 + const dow = getDay(todayKST); // 0=일, 6=토 + + return dow === 0 || dow === 6 ? addDays(monday, 7) : monday; +}; From 525cf7cb86e725a0a79421a0a6f776feff54bd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 16:30:09 +0900 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20coun?= =?UTF-8?q?t=EA=B0=80=20undefined=20=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/api/types.ts | 4 ++-- src/widgets/home/calendar.tsx | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/features/study/api/types.ts b/src/features/study/api/types.ts index a77cbd89..70fd16c9 100644 --- a/src/features/study/api/types.ts +++ b/src/features/study/api/types.ts @@ -59,8 +59,8 @@ export interface StudyCalendarDay { export interface MonthlyCalendarResponse { calendar: StudyCalendarDay[]; - monthlyCompletedCount: number; - totalCompletedCount: number; + monthlyCompletedCount?: number; + totalCompletedCount?: number; } export interface PostDailyRetrospectRequest { diff --git a/src/widgets/home/calendar.tsx b/src/widgets/home/calendar.tsx index 1d4769a0..402fd09d 100644 --- a/src/widgets/home/calendar.tsx +++ b/src/widgets/home/calendar.tsx @@ -79,6 +79,11 @@ const Calendar = (props: React.ComponentProps) => { if (isLoading) return
로딩 중...
; + const monthlyCount = data?.monthlyCompletedCount; + const totalCount = data?.totalCompletedCount; + const showFooter = + typeof monthlyCount === 'number' && typeof totalCount === 'number'; + return (
) => { day: 'text-center font-designer-14m rounded-full', }} footer={ -
-
- {month}월은 {data.monthlyCompletedCount}번의 스터디를 완료했어요. -
-
- 총 {data.totalCompletedCount}번의 스터디를 완료했어요. + showFooter ? ( +
+
+ {month}월은 {data.monthlyCompletedCount}번의 스터디를 + 완료했어요. +
+
+ 총 {data.totalCompletedCount}번의 스터디를 완료했어요. +
-
+ ) : undefined } {...props} /> From 718ffc34caa52d4820ed47da02db9013a96cd039 Mon Sep 17 00:00:00 2001 From: aken-you Date: Sat, 30 Aug 2025 16:37:01 +0900 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20satisfactionId=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/study-review-modal.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/study/ui/study-review-modal.tsx b/src/features/study/ui/study-review-modal.tsx index e39d325b..c5b8b85e 100644 --- a/src/features/study/ui/study-review-modal.tsx +++ b/src/features/study/ui/study-review-modal.tsx @@ -102,12 +102,12 @@ function StudyReviewForm({ onClose }: { onClose: () => void }) {
{ setForm({ ...form, - satisfactionId: 10, + satisfactionId: 30, keywordIds: [], content: '', }); @@ -130,12 +130,12 @@ function StudyReviewForm({ onClose }: { onClose: () => void }) { { setForm({ ...form, - satisfactionId: 30, + satisfactionId: 10, keywordIds: [], content: '', }); @@ -144,11 +144,11 @@ function StudyReviewForm({ onClose }: { onClose: () => void }) {
- {form.satisfactionId === 10 && ( + {form.satisfactionId === 30 && ( )} - {(form.satisfactionId === 20 || form.satisfactionId === 30) && ( + {(form.satisfactionId === 10 || form.satisfactionId === 20) && ( )} From 2c26b3ee6206c63a8d7cda1fd81a088f2683eb1e Mon Sep 17 00:00:00 2001 From: aken-you Date: Sat, 30 Aug 2025 16:52:41 +0900 Subject: [PATCH 04/12] =?UTF-8?q?fix:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=ED=9B=84=EA=B8=B0=20=EC=9A=94=EC=B2=AD=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20=EC=A0=84=EA=B9=8C=EC=A7=80=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20disabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/ui/study-review-modal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/study/ui/study-review-modal.tsx b/src/features/study/ui/study-review-modal.tsx index c5b8b85e..4bfd7ace 100644 --- a/src/features/study/ui/study-review-modal.tsx +++ b/src/features/study/ui/study-review-modal.tsx @@ -61,7 +61,7 @@ export default function StudyReviewModal({ function StudyReviewForm({ onClose }: { onClose: () => void }) { const { data } = usePartnerStudyReviewQuery(); - const { mutate: addStudyReview } = useAddStudyReviewMutation(); + const { mutate: addStudyReview, isPending } = useAddStudyReviewMutation(); const [form, setForm] = useState({ studySpaceId: data?.studySpaceId, @@ -162,7 +162,9 @@ function StudyReviewForm({ onClose }: { onClose: () => void }) { color="primary" size="large" disabled={ - form.keywordIds.length === 0 || form.satisfactionId === null + form.keywordIds.length === 0 || + form.satisfactionId === null || + isPending } onClick={handleSubmit} > From ce7fbf7ccef8b02d7ec33e2aa19e3911e4b6e4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 17:11:41 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20tooltip=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 1 + package.json | 1 + src/shared/ui/tooltip/index.tsx | 58 +++++++++++++++ yarn.lock | 128 ++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 src/shared/ui/tooltip/index.tsx diff --git a/next.config.ts b/next.config.ts index e40cb1e2..66af0467 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + output: 'standalone', /* config options here */ // 외부 이미지 도메인 허용 설정 추가 images: { diff --git a/package.json b/package.json index 83f1931e..d0ae9f0f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.0.6", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", diff --git a/src/shared/ui/tooltip/index.tsx b/src/shared/ui/tooltip/index.tsx new file mode 100644 index 00000000..63a933c5 --- /dev/null +++ b/src/shared/ui/tooltip/index.tsx @@ -0,0 +1,58 @@ +import * as RadixTooltip from '@radix-ui/react-tooltip'; +import * as React from 'react'; +import { cn } from '@/shared/shadcn/lib/utils'; + +interface TooltipProps { + trigger: React.ReactElement; + value: React.ReactNode; + side?: 'top' | 'right' | 'bottom' | 'left'; + align?: 'start' | 'center' | 'end'; + sideOffset?: number; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + contentClassName?: string; + arrowClassName?: string; +} + +const Tooltip: React.FC = ({ + trigger, + value, + side = 'top', + align = 'center', + sideOffset = 5, + open, + defaultOpen, + onOpenChange, + contentClassName = 'font-designer-16m rounded-100', + arrowClassName, +}) => { + return ( + + + {trigger} + + + + {value} + + + + + + ); +}; + +export default Tooltip; diff --git a/yarn.lock b/yarn.lock index 2f43b386..fb80f80e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2469,6 +2469,13 @@ __metadata: languageName: node linkType: hard +"@radix-ui/primitive@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/primitive@npm:1.1.3" + checksum: 10c0/88860165ee7066fa2c179f32ffcd3ee6d527d9dcdc0e8be85e9cb0e2c84834be8e3c1a976c74ba44b193f709544e12f54455d892b28e32f0708d89deda6b9f1d + languageName: node + linkType: hard + "@radix-ui/react-arrow@npm:1.1.7": version: 1.1.7 resolution: "@radix-ui/react-arrow@npm:1.1.7" @@ -2627,6 +2634,29 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dismissable-layer@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-escape-keydown": "npm:1.1.1" + 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/c825572a64073c4d3853702029979f6658770ffd6a98eabc4984e1dee1b226b4078a2a4dc7003f96475b438985e9b21a58e75f51db74dd06848dcae1f2d395dc + languageName: node + linkType: hard + "@radix-ui/react-dismissable-layer@npm:1.1.7": version: 1.1.7 resolution: "@radix-ui/react-dismissable-layer@npm:1.1.7" @@ -2809,6 +2839,34 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-popper@npm:1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-popper@npm:1.2.8" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + "@radix-ui/react-use-rect": "npm:1.1.1" + "@radix-ui/react-use-size": "npm:1.1.1" + "@radix-ui/rect": "npm:1.1.1" + 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/48e3f13eac3b8c13aca8ded37d74db17e1bb294da8d69f142ab6b8719a06c3f90051668bed64520bf9f3abdd77b382ce7ce209d056bb56137cecc949b69b421c + languageName: node + linkType: hard + "@radix-ui/react-portal@npm:1.1.6": version: 1.1.6 resolution: "@radix-ui/react-portal@npm:1.1.6" @@ -2869,6 +2927,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-presence@npm:1.1.5": + version: 1.1.5 + resolution: "@radix-ui/react-presence@npm:1.1.5" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + 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/d0e61d314250eeaef5369983cb790701d667f51734bafd98cf759072755562018052c594e6cdc5389789f4543cb0a4d98f03ff4e8f37338d6b5bf51a1700c1d1 + languageName: node + linkType: hard + "@radix-ui/react-primitive@npm:2.1.0": version: 2.1.0 resolution: "@radix-ui/react-primitive@npm:2.1.0" @@ -3064,6 +3142,36 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-tooltip@npm:^1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-tooltip@npm:1.2.8" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + "@radix-ui/react-visually-hidden": "npm:1.2.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/de0cbae9c571a00671f160928d819e59502f59be8749f536ab4b180181d9d70aee3925a5b2555f8f32d0bea622bc35f65b70ca7ff0449e4844f891302310cc48 + languageName: node + linkType: hard + "@radix-ui/react-use-callback-ref@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1" @@ -3194,6 +3302,25 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-visually-hidden@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-visually-hidden@npm:1.2.3" + dependencies: + "@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/cf86a37f1cbee50a964056f3dc4f6bb1ee79c76daa321f913aa20ff3e1ccdfafbf2b114d7bb616aeefc7c4b895e6ca898523fdb67710d89bd5d8edb739a0d9b6 + languageName: node + linkType: hard + "@radix-ui/rect@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/rect@npm:1.1.1" @@ -10629,6 +10756,7 @@ __metadata: "@radix-ui/react-slot": "npm:^1.2.2" "@radix-ui/react-switch": "npm:^1.2.4" "@radix-ui/react-toggle": "npm:^1.1.9" + "@radix-ui/react-tooltip": "npm:^1.2.8" "@rushstack/eslint-config": "npm:^4.1.1" "@storybook/addon-essentials": "npm:^8.6.12" "@storybook/addon-onboarding": "npm:^8.6.12" From 06f9a11e5a7f982332086ffc6fd3cec981c1952f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 17:11:49 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=EC=84=B1=EC=8B=A4=EC=98=A8?= =?UTF-8?q?=EB=8F=84=20=EA=B4=80=EB=A0=A8=20=ED=88=B4=ED=8C=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/my-page/ui/profile.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/features/my-page/ui/profile.tsx b/src/features/my-page/ui/profile.tsx index f1db1e9e..518e3571 100644 --- a/src/features/my-page/ui/profile.tsx +++ b/src/features/my-page/ui/profile.tsx @@ -10,6 +10,7 @@ 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'; +import Tooltip from '@/shared/ui/tooltip'; interface ProfileProps { memberId: number; @@ -80,12 +81,21 @@ export default function Profile({
- 성실 온도 - 성실온도 설명 + 성실 온도 + + + } />
Date: Sat, 30 Aug 2025 17:15:48 +0900 Subject: [PATCH 07/12] =?UTF-8?q?fix:=20=EB=82=98=EC=A4=91=EC=97=90=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=EC=8B=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=94=EA=B0=80.=20standalone=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index 66af0467..0ba97d79 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - output: 'standalone', + // output: 'standalone', /* config options here */ // 외부 이미지 도메인 허용 설정 추가 images: { From 0b6f729fdbe97b7a569c9197a78a5ad58c67c2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 17:27:41 +0900 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=95=88=EB=90=98=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/dropdown/multi.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/shared/ui/dropdown/multi.tsx b/src/shared/ui/dropdown/multi.tsx index fc1fc0ca..63d47850 100644 --- a/src/shared/ui/dropdown/multi.tsx +++ b/src/shared/ui/dropdown/multi.tsx @@ -42,10 +42,10 @@ export default function MultiDropdown({ [value], ); - const selectedLabels = useMemo(() => { + const selectedItems = useMemo(() => { const set = new Set(selected); - return options.filter((o) => set.has(o.value)).map((o) => o.label); + return options.filter((o) => set.has(o.value)); }, [options, selected]); const remainingOptions = useMemo(() => { @@ -99,20 +99,20 @@ export default function MultiDropdown({ )} - {selectedLabels.map((label, idx) => ( + {selectedItems.map((item) => ( - {label} + {item.label} From 8abffe58ab9d29653d891727ed15555f02ba20bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 18:23:59 +0900 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20=EB=AF=B8=EB=A6=AC=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=ED=95=98=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/ui/user-profile-modal.tsx | 330 ++++++++++-------- .../ui/reservation-user-card.tsx | 1 - 2 files changed, 188 insertions(+), 143 deletions(-) diff --git a/src/entities/user/ui/user-profile-modal.tsx b/src/entities/user/ui/user-profile-modal.tsx index a9ab7436..041aaa84 100644 --- a/src/entities/user/ui/user-profile-modal.tsx +++ b/src/entities/user/ui/user-profile-modal.tsx @@ -1,6 +1,7 @@ 'use client'; import { XIcon } from 'lucide-react'; +import { useState } from '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'; @@ -23,167 +24,212 @@ export default function UserProfileModal({ memberId, trigger, }: UserProfileModalProps) { + const [open, setOpen] = useState(false); + + return ( + + {trigger} + + {open && ( + + + + setOpen(false)} + /> + + + )} + + ); +} + +function UserProfileBody({ + memberId, + onClose, +}: { + memberId: number; + onClose: () => void; +}) { const { data: profile, isLoading, isError } = useUserProfileQuery(memberId); const { data: positiveKeywordsData } = useUserPositiveKeywordsQuery({ memberId, }); - if (isLoading || isError || !profile || !positiveKeywordsData) return null; - - const positiveKeywords = positiveKeywordsData?.keywords || []; + if (isLoading) { + return ( + <> +
+ 불러오는 중… + + ); + } + + if (isError || !profile || !positiveKeywordsData) { + return ( + <> +
+ 프로필을 불러오지 못했습니다. + + ); + } + + const positiveKeywords = positiveKeywordsData.keywords ?? []; const temperPreset = getSincerityPresetByLevelName( profile.sincerityTemp.levelName, ); return ( - - {trigger} - - - - - - {profile.memberProfile.memberName}님의 프로필 - - - - - - - -
- +
+ + +
+ + +
+
+ {profile.memberProfile.mbti && ( + {profile.memberProfile.mbti} + )} + {profile.memberProfile.interests.slice(0, 4).map((interest) => ( + + {interest.name} + + ))} +
+ +
+
+ {profile.memberProfile.memberName} +
+ +
+ +
+ + t.techStackName) + .join(', ')} + /> + t.label) + .join(', ')} + /> + + +
+ +
+ +
+ + 받은 평가 + + +
+ {/* todo: 기획 fix되면 수정 */} + {/* n명의 유저들이 이런 점이 좋다고 했어요. */} +
    + {positiveKeywords.length > 0 ? ( + positiveKeywords.map((keyword) => ( + + )) + ) : ( + + 아직 받은 평가가 없습니다. + + )} +
+
+
+ + + ); +} -
- -
- - 받은 평가 - - -
- {/* todo: 기획 fix되면 수정 */} - {/* n명의 유저들이 이런 점이 좋다고 했어요. */} - -
    - {positiveKeywords.length > 0 ? ( - positiveKeywords.map((keyword) => ( - - )) - ) : ( - - 아직 받은 평가가 없습니다. - - )} -
-
-
- - - - +function Header({ title, onClose }: { title: string; onClose: () => void }) { + return ( + + + {title} + + + + + + ); +} + +function Field({ icon, value }: { icon: React.ReactNode; value?: string }) { + return ( +
+ {icon} + + {value ?? ''} + +
); } diff --git a/src/features/study/participation/ui/reservation-user-card.tsx b/src/features/study/participation/ui/reservation-user-card.tsx index 4e23e6b3..fc0aa743 100644 --- a/src/features/study/participation/ui/reservation-user-card.tsx +++ b/src/features/study/participation/ui/reservation-user-card.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import UserProfileModal from '@/entities/user/ui/user-profile-modal'; import UserAvatar from '@/shared/ui/avatar'; import Badge from '@/shared/ui/badge'; From 449fa3550752474feab3a8affa0a0f1b5093120f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 18:25:16 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=ED=86=A0=EA=B8=80=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EB=AA=A9=EB=A1=9D=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/model/use-user-profile-query.ts | 72 ++++++++++++++----- src/features/study/model/use-study-query.ts | 1 + .../participation/ui/reservation-list.tsx | 29 ++------ src/features/study/ui/study-card.tsx | 2 +- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/entities/user/model/use-user-profile-query.ts b/src/entities/user/model/use-user-profile-query.ts index 374fbcf9..d31a5f19 100644 --- a/src/entities/user/model/use-user-profile-query.ts +++ b/src/entities/user/model/use-user-profile-query.ts @@ -1,13 +1,10 @@ import { sendGTMEvent } from '@next/third-parties/google'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getUserProfile, patchAutoMatching, } from '@/entities/user/api/get-user-profile'; -import type { - GetUserProfileResponse, - PatchAutoMatchingParams, -} from '@/entities/user/api/types'; +import type { GetUserProfileResponse } from '@/entities/user/api/types'; import { hashValue } from '@/shared/lib/hash'; export const useUserProfileQuery = (memberId: number) => { @@ -19,23 +16,62 @@ export const useUserProfileQuery = (memberId: number) => { }); }; +export interface PatchAutoMatchingParams { + memberId: number; + autoMatching: boolean; +} + export const usePatchAutoMatchingMutation = () => { - return useMutation({ + const qc = useQueryClient(); + + return useMutation< + void, + unknown, + PatchAutoMatchingParams, + { prev?: unknown } + >({ mutationFn: patchAutoMatching, - onSuccess: (_, variables) => { - if (variables.autoMatching) { - sendGTMEvent({ - event: 'custom_member_study_toggle_on', - dl_timestamp: new Date().toISOString(), - dl_member_id: hashValue(String(variables.memberId)), - }); - } else { - sendGTMEvent({ - event: 'custom_member_study_toggle_off', - dl_timestamp: new Date().toISOString(), - dl_member_id: hashValue(String(variables.memberId)), + + onMutate: async ({ memberId, autoMatching }) => { + await qc.cancelQueries({ queryKey: ['userProfile', memberId] }); + const prev = qc.getQueryData(['userProfile', memberId]); + if (prev && typeof prev === 'object') { + qc.setQueryData(['userProfile', memberId], { + ...(prev as any), + autoMatching, }); } + + return { prev }; + }, + + onError: (_err, { memberId }, ctx) => { + if (ctx?.prev) { + qc.setQueryData(['userProfile', memberId], ctx.prev); + } + }, + + onSuccess: async (_data, { memberId, autoMatching }) => { + await qc.invalidateQueries({ queryKey: ['userProfile', memberId] }); + + await qc.invalidateQueries({ + predicate: (q) => + Array.isArray(q.queryKey) && q.queryKey[0] === 'weeklyParticipation', + }); + + await qc.invalidateQueries({ + predicate: (q) => + Array.isArray(q.queryKey) && + q.queryKey[0] === 'weeklyReservationMembers', + }); + + sendGTMEvent({ + event: autoMatching + ? 'custom_member_study_toggle_on' + : 'custom_member_study_toggle_off', + dl_timestamp: new Date().toISOString(), + dl_member_id: hashValue(String(memberId)), + }); }, }); }; diff --git a/src/features/study/model/use-study-query.ts b/src/features/study/model/use-study-query.ts index ab782631..8976355c 100644 --- a/src/features/study/model/use-study-query.ts +++ b/src/features/study/model/use-study-query.ts @@ -23,6 +23,7 @@ export const useWeeklyParticipation = (params: string) => { queryKey: ['weeklyParticipation', params], queryFn: () => getWeeklyParticipation(params), staleTime: 60 * 1000, + enabled: !!params, }); }; diff --git a/src/features/study/participation/ui/reservation-list.tsx b/src/features/study/participation/ui/reservation-list.tsx index 284e9206..9a84875c 100644 --- a/src/features/study/participation/ui/reservation-list.tsx +++ b/src/features/study/participation/ui/reservation-list.tsx @@ -9,24 +9,24 @@ import { import ProfileDefault from '@/entities/user/ui/icon/profile-default.svg'; import { getCookie } from '@/shared/tanstack-query/cookie'; import ReservationCard from './reservation-user-card'; +import { useWeeklyParticipation } from '../../model/use-study-query'; import StartStudyModal from '../../ui/start-study-modal'; import { useInfiniteReservation } from '../model/use-participation-query'; interface ReservationListProps { - isParticipation?: boolean; + studyDate?: string; pageSize?: number; month: number; week: number; } export default function ReservationList({ - isParticipation = false, + studyDate, pageSize = 50, month, week, }: ReservationListProps) { const sentinelRef = useRef(null); - const calledRef = useRef(false); const [memberId, setMemberId] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -35,37 +35,22 @@ export default function ReservationList({ setMemberId(id ? Number(id) : null); }, []); + const { data: participation } = useWeeklyParticipation(studyDate); + const isParticipation = participation?.isParticipate ?? false; + const firstMemberId = useMemo( () => (isParticipation && memberId !== null ? memberId : null), [isParticipation, memberId], ); const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = - useInfiniteReservation(firstMemberId, pageSize); + useInfiniteReservation(firstMemberId ?? undefined, pageSize); const { data: userProfile } = useUserProfileQuery(memberId ?? 0); const { mutate: patchAutoMatching, isPending } = usePatchAutoMatchingMutation(); - useEffect(() => { - if (!memberId || isParticipation || !userProfile) return; - - const { studyApplied, autoMatching } = userProfile; - - if (studyApplied && !autoMatching && !calledRef.current) { - calledRef.current = true; - patchAutoMatching( - { memberId, autoMatching: true }, - { - onError: () => { - calledRef.current = false; - }, - }, - ); - } - }, [memberId, isParticipation, userProfile, patchAutoMatching]); - useEffect(() => { if (!hasNextPage) return; diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/ui/study-card.tsx index 04082f97..453ccd4c 100644 --- a/src/features/study/ui/study-card.tsx +++ b/src/features/study/ui/study-card.tsx @@ -80,7 +80,7 @@ export default function StudyCard() { )} From 669c862cad5cce61c845097bc1edbd3739822169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sat, 30 Aug 2025 20:04:34 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20=EB=B3=B8=EC=9D=B8=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../participation/ui/reservation-list.tsx | 19 +++++++++++++------ .../ui/reservation-user-card.tsx | 13 ++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/features/study/participation/ui/reservation-list.tsx b/src/features/study/participation/ui/reservation-list.tsx index 9a84875c..1dee8a28 100644 --- a/src/features/study/participation/ui/reservation-list.tsx +++ b/src/features/study/participation/ui/reservation-list.tsx @@ -38,16 +38,19 @@ export default function ReservationList({ const { data: participation } = useWeeklyParticipation(studyDate); const isParticipation = participation?.isParticipate ?? false; + const { data: userProfile } = useUserProfileQuery(memberId ?? 0); + const autoMatching = userProfile?.autoMatching ?? false; + + const applied = autoMatching || isParticipation; + const firstMemberId = useMemo( - () => (isParticipation && memberId !== null ? memberId : null), - [isParticipation, memberId], + () => (applied && memberId !== null ? memberId : null), + [applied, memberId], ); const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteReservation(firstMemberId ?? undefined, pageSize); - const { data: userProfile } = useUserProfileQuery(memberId ?? 0); - const { mutate: patchAutoMatching, isPending } = usePatchAutoMatchingMutation(); @@ -109,10 +112,14 @@ export default function ReservationList({
{items.map((p) => ( - + ))} - {!isParticipation && ( + {!applied && (
{participant.name}
- {isCurrentUser && 본인} + {isCurrentUser && ( + + 본인 + + )}
{participant.simpleIntroduction} From 7951f791352350665ea404b1eca38ad20d9b5aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EB=AF=BC=EC=A3=BC?= Date: Sun, 31 Aug 2025 00:16:22 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20=ED=86=A0=EA=B8=80=20on/off?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B2=84=ED=8A=BC=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/participation/ui/reservation-list.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/features/study/participation/ui/reservation-list.tsx b/src/features/study/participation/ui/reservation-list.tsx index 1dee8a28..912afdef 100644 --- a/src/features/study/participation/ui/reservation-list.tsx +++ b/src/features/study/participation/ui/reservation-list.tsx @@ -9,7 +9,6 @@ import { import ProfileDefault from '@/entities/user/ui/icon/profile-default.svg'; import { getCookie } from '@/shared/tanstack-query/cookie'; import ReservationCard from './reservation-user-card'; -import { useWeeklyParticipation } from '../../model/use-study-query'; import StartStudyModal from '../../ui/start-study-modal'; import { useInfiniteReservation } from '../model/use-participation-query'; @@ -35,17 +34,12 @@ export default function ReservationList({ setMemberId(id ? Number(id) : null); }, []); - const { data: participation } = useWeeklyParticipation(studyDate); - const isParticipation = participation?.isParticipate ?? false; - const { data: userProfile } = useUserProfileQuery(memberId ?? 0); const autoMatching = userProfile?.autoMatching ?? false; - const applied = autoMatching || isParticipation; - const firstMemberId = useMemo( - () => (applied && memberId !== null ? memberId : null), - [applied, memberId], + () => (autoMatching && memberId !== null ? memberId : null), + [autoMatching, memberId], ); const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = @@ -119,7 +113,7 @@ export default function ReservationList({ /> ))} - {!applied && ( + {!autoMatching && (