Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/app/(landing)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import '../global.css';

import Clarity from '@microsoft/clarity';
import { GoogleTagManager } from '@next/third-parties/google';
import { clsx } from 'clsx';
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import ClarityInit from '@/components/analytics/clarity-init';
import PageViewTracker from '@/components/analytics/page-view-tracker';
import MainProvider from '@/providers';
import { getOrganizationSchema, getWebsiteSchema } from '@/utils/seo';
Expand Down Expand Up @@ -62,10 +62,6 @@ export default async function LandingPageLayout({
children: React.ReactNode;
}>) {
const initialAccessToken = await getServerCookie('accessToken');
if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) {
Clarity.init(CLARITY_PROJECT_ID);
}

const organizationSchema = getOrganizationSchema();
const websiteSchema = getWebsiteSchema();

Expand Down Expand Up @@ -94,6 +90,7 @@ export default async function LandingPageLayout({
</head>
<body className={clsx(pretendard.className, 'h-screen w-screen')}>
<MainProvider initialAccessToken={initialAccessToken ?? undefined}>
<ClarityInit projectId={CLARITY_PROJECT_ID} />
<PageViewTracker />
<div className="w-full overflow-auto">
{/** 1400 + 48*2 패딩 양옆 48로 임의적용 */}
Expand Down
6 changes: 2 additions & 4 deletions src/app/(service)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import '../global.css';

import Clarity from '@microsoft/clarity';
import { GoogleTagManager } from '@next/third-parties/google';
import { clsx } from 'clsx';
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import React from 'react';
import ClarityInit from '@/components/analytics/clarity-init';
import PageViewTracker from '@/components/analytics/page-view-tracker';
import MainProvider from '@/providers';
import { getServerCookie } from '@/utils/server-cookie';
Expand Down Expand Up @@ -34,15 +34,13 @@ export default async function ServiceLayout({
children: React.ReactNode;
}>) {
const initialAccessToken = await getServerCookie('accessToken');
if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) {
Clarity.init(CLARITY_PROJECT_ID);
}

return (
<html lang="ko">
<head>{GTM_ID && <GoogleTagManager gtmId={GTM_ID} />}</head>
<body className={clsx(pretendard.className, 'min-h-screen w-screen')}>
<MainProvider initialAccessToken={initialAccessToken ?? undefined}>
<ClarityInit projectId={CLARITY_PROJECT_ID} />
<PageViewTracker />
<div className="flex min-h-screen w-full flex-col overflow-x-auto">
<Header />
Expand Down
29 changes: 29 additions & 0 deletions src/components/analytics/clarity-init.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';

import Clarity from '@microsoft/clarity';
import { useEffect } from 'react';
import type { ReactElement } from 'react';

declare global {
interface Window {
__clarityInitialized?: boolean;
}
}

interface ClarityInitProps {
projectId?: string;
}

export default function ClarityInit({
projectId,
}: ClarityInitProps): ReactElement {
useEffect(() => {
if (!projectId || typeof window === 'undefined') return;
if (window.__clarityInitialized) return;

Clarity.init(projectId);
window.__clarityInitialized = true;
}, [projectId]);

return <></>;
}
7 changes: 4 additions & 3 deletions src/components/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { cn } from '@/components/ui/(shadcn)/lib/utils';

const EXIT_DURATION = 200;

interface ToastProps {
message: string;
isVisible: boolean;
Expand All @@ -22,7 +24,6 @@ export default function Toast({
}: ToastProps) {
const [isRendered, setIsRendered] = useState(isVisible);
const [isExiting, setIsExiting] = useState(false);
const exitDuration = 200;

useEffect(() => {
if (isVisible) {
Expand All @@ -40,11 +41,11 @@ export default function Toast({
const timer = setTimeout(() => {
setIsRendered(false);
setIsExiting(false);
}, exitDuration);
}, EXIT_DURATION);

return () => clearTimeout(timer);
}
}, [isVisible, duration, onClose]);
}, [isVisible, duration, onClose, isRendered]);

if (!isRendered) return null;
if (typeof document === 'undefined') return null;
Expand Down
13 changes: 9 additions & 4 deletions src/features/phone-verification/model/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ interface PhoneVerificationState {
setHasHydrated: (hasHydrated: boolean) => void;
}

type PersistedPhoneVerificationState = Pick<
PhoneVerificationState,
'isVerified' | 'phoneNumber' | 'verifiedAt' | 'memberId'
>;

export const usePhoneVerificationStore = create<PhoneVerificationState>()(
persist(
(set) => ({
Expand All @@ -21,12 +26,12 @@ export const usePhoneVerificationStore = create<PhoneVerificationState>()(
memberId: null as number | null,
hasHydrated: false,
setVerified: (phoneNumber, memberId) =>
set({
set((state) => ({
isVerified: true,
phoneNumber,
verifiedAt: new Date().toISOString(),
...(memberId !== undefined ? { memberId } : {}),
}),
memberId: memberId ?? state.memberId,
})),
reset: () =>
set({
isVerified: false,
Expand All @@ -38,7 +43,7 @@ export const usePhoneVerificationStore = create<PhoneVerificationState>()(
}),
{
name: 'phone-verification-storage',
partialize: (state) => ({
partialize: (state): PersistedPhoneVerificationState => ({
isVerified: state.isVerified,
phoneNumber: state.phoneNumber,
verifiedAt: state.verifiedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ export const useVerifyPhoneCodeMutation = (memberId?: number) => {
mutationFn: verifyPhoneCode,
onSuccess: async (data, variables) => {
if (data.success) {
// 현재 사용자의 memberId 가져오기 (쿠키에서)
const currentMemberId = memberId ?? Number(getCookie('memberId'));

// 인증 상태 저장 (memberId 포함)
// 인증 상태 저장
setVerified(variables.phoneNumber, currentMemberId || undefined);

// 프로필 정보 쿼리키 갱신
if (currentMemberId) {
await queryClient.invalidateQueries({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ export function usePhoneVerificationStatus(

// 서버 데이터 기반 인증 상태 계산
const serverHasIsVerified = typeof userProfile?.isVerified === 'boolean';
const serverIsVerified = serverHasIsVerified
? userProfile!.isVerified
: false;
const serverIsVerified = serverHasIsVerified ? userProfile.isVerified : false;
const serverPhoneNumber = userProfile?.memberProfile?.tel ?? null;

// 스토어가 현재 유저 소유인지 확인 (계정 전환 대응)
Expand Down Expand Up @@ -153,22 +151,22 @@ export function usePhoneVerificationStatus(
current.memberId !== memberId ||
current.phoneNumber !== serverPhoneNumber
) {
store.setVerified(serverPhoneNumber ?? '', memberId);
}
} else {
// 서버가 미인증 → 같은 유저 캐시 제거
if (current.isVerified && current.memberId === memberId) {
store.reset();
current.setVerified(serverPhoneNumber ?? '', memberId);
}

return;
}

// 서버가 미인증 → 같은 유저 캐시 제거
if (current.isVerified && current.memberId === memberId) {
current.reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isProfileLoading,
userProfile,
memberId,
serverIsVerified,
serverPhoneNumber,
resolvedServerIsVerified,
serverPhoneNumber,
]);

// memberId 변경(계정 전환 / 로그아웃) 시 스토어 리셋
Expand All @@ -178,17 +176,17 @@ export function usePhoneVerificationStatus(
prevMemberIdRef.current = memberId;

if (prev !== null && prev !== memberId) {
store.reset();
usePhoneVerificationStore.getState().reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [memberId]);

// 인증 완료 시 호출 — memberId를 자동 주입
const setVerified = useCallback(
(phone: string) => {
store.setVerified(phone, memberId ?? undefined);
usePhoneVerificationStore
.getState()
.setVerified(phone, memberId ?? undefined);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[memberId],
);

Expand Down
34 changes: 23 additions & 11 deletions src/features/study/group/ui/group-study-form-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ export default function GroupStudyFormModal({
};

const refineStudyDetail = (value: GroupStudyFullResponseDto) => {
if (isGroupStudyLoading) return;

const refinedClassification =
value.basicInfo?.classification ?? classification;
const originalType = value.basicInfo?.type;
Expand All @@ -116,7 +114,7 @@ export default function GroupStudyFormModal({
return {
classification: refinedClassification,
studyLeaderParticipation:
value.basicInfo.studyLeaderParticipation ?? false,
value.basicInfo?.studyLeaderParticipation ?? false,
type: refinedType,
targetRoles: value.basicInfo?.targetRoles,
maxMembersCount: value.basicInfo?.maxMembersCount?.toString() ?? '',
Expand Down Expand Up @@ -232,6 +230,11 @@ export default function GroupStudyFormModal({
}
}, [open, mode]);

const editDefaultValues =
mode === 'edit' && groupStudyInfo
? refineStudyDetail(groupStudyInfo)
: null;

return (
<>
<Modal.Root
Expand All @@ -250,14 +253,23 @@ export default function GroupStudyFormModal({
<XIcon />
</Modal.Close>
</Modal.Header>
<GroupStudyForm
defaultValues={
mode === 'create'
? buildOpenGroupDefaultValues(classification)
: refineStudyDetail(groupStudyInfo!)
}
onSubmit={handleSubmitForm}
/>
{mode === 'create' && (
<GroupStudyForm
defaultValues={buildOpenGroupDefaultValues(classification)}
onSubmit={handleSubmitForm}
/>
)}
{mode === 'edit' && isGroupStudyLoading && (
<Modal.Body className="font-designer-16m text-text-subtle py-800 text-center">
스터디 정보를 불러오는 중입니다...
</Modal.Body>
)}
{mode === 'edit' && !isGroupStudyLoading && editDefaultValues && (
<GroupStudyForm
defaultValues={editDefaultValues}
onSubmit={handleSubmitForm}
/>
Comment on lines +262 to +271
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

편집 모드에서 데이터 미존재 시 빈 화면 상태가 발생합니다.
isGroupStudyLoading이 false인데 editDefaultValues가 null이면 모달 바디가 비어 보입니다(예: 조회 실패/데이터 없음). 에러/빈 상태 메시지 또는 재시도 UI를 추가해 주세요.

🤖 Prompt for AI Agents
In `@src/features/study/group/ui/group-study-form-modal.tsx` around lines 262 -
271, When in edit mode the code only renders GroupStudyForm when
editDefaultValues exists, causing an empty modal if isGroupStudyLoading is false
and editDefaultValues is null; add a fallback branch for mode === 'edit' &&
!isGroupStudyLoading && !editDefaultValues that renders a Modal.Body with an
error/empty state message and a retry action (e.g., a Retry button that calls
your existing refetch function or a new handler like refetchGroupStudy) and/or a
close option so the user isn’t left with a blank modal; update the conditional
rendering around isGroupStudyLoading/editDefaultValues/GroupStudyForm and wire
the retry to the appropriate handler (or expose a new onRetry prop) instead of
leaving the modal empty.

)}
</Modal.Content>
</Modal.Portal>
</Modal.Root>
Expand Down
Loading