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: 5 additions & 2 deletions src/app/(admin)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import localFont from 'next/font/local';
import PageViewTracker from '@/components/analytics/page-view-tracker';
import AdminSideBar from '@/components/layout/sidebar/admin-sidebar';
import MainProvider from '@/providers';
import { getServerCookie } from '@/utils/server-cookie';

export const metadata: Metadata = {
title: '관리자 - ZERO-ONE',
Expand All @@ -28,16 +29,18 @@ const pretendard = localFont({

const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;

export default function AdminLayout({
export default async function AdminLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const initialAccessToken = await getServerCookie('accessToken');

return (
<html lang="ko">
<head>{GTM_ID && <GoogleTagManager gtmId={GTM_ID} />}</head>
<body className={clsx(pretendard.className, 'h-screen w-screen')}>
<MainProvider>
<MainProvider initialAccessToken={initialAccessToken ?? undefined}>
<PageViewTracker />
<div className="flex min-w-[1200px]">
<AdminSideBar />
Expand Down
6 changes: 4 additions & 2 deletions src/app/(landing)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import localFont from 'next/font/local';
import PageViewTracker from '@/components/analytics/page-view-tracker';
import MainProvider from '@/providers';
import { getOrganizationSchema, getWebsiteSchema } from '@/utils/seo';
import { getServerCookie } from '@/utils/server-cookie';
import Header from '@/widgets/home/header';

export const metadata: Metadata = {
Expand Down Expand Up @@ -55,11 +56,12 @@ const pretendard = localFont({
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;
const CLARITY_PROJECT_ID = process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID;

export default function LandingPageLayout({
export default async function LandingPageLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const initialAccessToken = await getServerCookie('accessToken');
if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) {
Clarity.init(CLARITY_PROJECT_ID);
}
Comment on lines 65 to 67
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

서버 컴포넌트에서 Clarity 초기화 코드가 실행되지 않습니다.

async function 레이아웃은 서버에서만 실행되므로 typeof window !== 'undefined' 조건이 항상 false가 되어 Clarity.init()이 절대 호출되지 않습니다. Clarity 초기화는 클라이언트 컴포넌트나 별도의 useEffect 훅으로 이동해야 합니다.

🤖 Prompt for AI Agents
In `@src/app/`(landing)/layout.tsx around lines 65 - 67, The Clarity
initialization inside the async server-only layout never runs because typeof
window !== 'undefined' is always false on the server; move the
Clarity.init(CLARITY_PROJECT_ID) call into a client-side context: create a
client component (or a useEffect within a client component) that checks
CLARITY_PROJECT_ID and calls Clarity.init, then import/render that client
component from your layout; update references to Clarity.init and
CLARITY_PROJECT_ID in the new client component so initialization happens only on
the client.

Expand Down Expand Up @@ -91,7 +93,7 @@ export default function LandingPageLayout({
<link rel="manifest" href="/manifest.json" />
</head>
<body className={clsx(pretendard.className, 'h-screen w-screen')}>
<MainProvider>
<MainProvider initialAccessToken={initialAccessToken ?? undefined}>
<PageViewTracker />
<div className="w-full overflow-auto">
{/** 1400 + 48*2 패딩 양옆 48로 임의적용 */}
Expand Down
2 changes: 2 additions & 0 deletions src/app/(service)/(my)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Metadata } from 'next';
import GlobalToast from '@/components/ui/global-toast';
import Sidebar from '@/widgets/my-page/sidebar';

export const metadata: Metadata = {
Expand All @@ -13,6 +14,7 @@ export default function MyLayout({
}>) {
return (
<div className="flex h-full">
<GlobalToast />
<Sidebar />
<div className="m-auto pt-500 pb-[100px]">
<div className="w-[780px]">{children}</div>
Expand Down
6 changes: 3 additions & 3 deletions src/app/(service)/(my)/my-study/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import { useMemberStudyListQuery } from '@/features/study/group/model/use-member
import CompletedGroupStudyList from '@/features/study/group/ui/completed-group-study-list';
import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal';
import NotCompletedGroupStudyList from '@/features/study/group/ui/not-completed-group-study-list';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';

interface MemberGroupStudyList extends MemberStudyItem {
type: 'GROUP_STUDY';
}

export default function MyStudy() {
const { data: authData } = useAuth();
const { memberId } = useAuthReady();

const { data, isLoading } = useMemberStudyListQuery({
memberId: authData?.memberId,
memberId,
studyType: 'GROUP_STUDY',
studyStatus: 'BOTH',
});
Comment on lines +18 to 24
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

🧩 Analysis chain

🏁 Script executed:

# Find the MemberStudyListRequest type definition
fd --type f -e ts -e tsx | xargs rg "interface MemberStudyListRequest|type MemberStudyListRequest" -A 5

Repository: code-zero-to-one/study-platform-client

Length of output: 634


🏁 Script executed:

# Check the useAuthReady hook return type
fd --type f -e ts -e tsx -path "*hooks*use-auth*" | head -5

Repository: code-zero-to-one/study-platform-client

Length of output: 250


🏁 Script executed:

# Find useMemberStudyListQuery implementation
fd --type f -e ts -e tsx | xargs rg "useMemberStudyListQuery|MemberStudyListQuery" -A 10 -B 2 | head -80

Repository: code-zero-to-one/study-platform-client

Length of output: 7220


🏁 Script executed:

# Check the actual code in the target file
cat -n src/app/\(service\)/\(my\)/my-study/page.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 3505


🏁 Script executed:

# Check my-participating-studies-section.tsx to see how it handles memberId
sed -n '40,50p' src/components/section/my-participating-studies-section.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 523


🏁 Script executed:

# Check useAuthReady hook return type
cat -n src/hooks/common/use-auth.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 4646


🏁 Script executed:

# Check useMemberStudyListQuery implementation to see how it handles the parameters
cat -n src/features/study/group/model/use-member-study-list-query.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 1256


memberId의 타입 불일치: undefined | numbernumber로 전달

useAuthReady()에서 반환되는 memberId는 hydration 이전에 undefined일 수 있습니다. MemberStudyListRequestmemberId: number로 정의되어 있어 타입 호환성 문제가 발생합니다. useMemberStudyListQueryenabled: memberId > 0 조건이 런타임 안전성을 보장하지만, 타입 검사 단계에서 오류가 발생할 수 있습니다.

my-participating-studies-section.tsx에서 사용되는 패턴(memberId ?? 0)을 적용하여 타입 안전성을 확보하세요.

🤖 Prompt for AI Agents
In `@src/app/`(service)/(my)/my-study/page.tsx around lines 18 - 24, memberId from
useAuthReady() can be undefined during hydration but useMemberStudyListQuery
expects a number (MemberStudyListRequest.memberId); update the call to pass a
non-undefined number (same pattern used in my-participating-studies-section.tsx)
by coercing memberId to 0 when undefined and keep the existing enabled guard
(e.g., pass memberId ?? 0 into useMemberStudyListQuery while leaving enabled:
memberId > 0) so TypeScript types align and runtime behavior is unchanged.

Expand Down
14 changes: 14 additions & 0 deletions src/app/(service)/group-study/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import GlobalToast from '@/components/ui/global-toast';

export default function GroupStudyLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<GlobalToast />
{children}
</>
);
}
2 changes: 2 additions & 0 deletions src/app/(service)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Metadata } from 'next';
import StartStudyButton from '@/components/home/start-study-button';
import GlobalToast from '@/components/ui/global-toast';
import { generateMetadata as generateSEOMetadata } from '@/utils/seo';
import Banner from '@/widgets/home/banner';
import FeedbackLink from '@/widgets/home/feedback-link';
Expand All @@ -24,6 +25,7 @@ export default async function Home({

return (
<div className="mx-auto flex w-[1496px] flex-col gap-500 px-600 py-600">
<GlobalToast />
<Banner />
<FeedbackLink />
<StartStudyButton />
Expand Down
8 changes: 4 additions & 4 deletions src/app/(service)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type { Metadata } from 'next';
import localFont from 'next/font/local';
import React from 'react';
import PageViewTracker from '@/components/analytics/page-view-tracker';
import GlobalToast from '@/components/ui/global-toast';
import MainProvider from '@/providers';
import { getServerCookie } from '@/utils/server-cookie';
import Header from '@/widgets/home/header';

export const metadata: Metadata = {
Expand All @@ -28,11 +28,12 @@ const pretendard = localFont({
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;
const CLARITY_PROJECT_ID = process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID;

export default function ServiceLayout({
export default async function ServiceLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const initialAccessToken = await getServerCookie('accessToken');
if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) {
Clarity.init(CLARITY_PROJECT_ID);
}
Expand All @@ -41,8 +42,7 @@ export default function ServiceLayout({
<html lang="ko">
<head>{GTM_ID && <GoogleTagManager gtmId={GTM_ID} />}</head>
<body className={clsx(pretendard.className, 'min-h-screen w-screen')}>
<MainProvider>
<GlobalToast />
<MainProvider initialAccessToken={initialAccessToken ?? undefined}>
<PageViewTracker />
<div className="flex min-h-screen w-full flex-col overflow-x-auto">
<Header />
Expand Down
14 changes: 14 additions & 0 deletions src/app/(service)/premium-study/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import GlobalToast from '@/components/ui/global-toast';

export default function PremiumStudyLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<GlobalToast />
{children}
</>
);
}
32 changes: 32 additions & 0 deletions src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,38 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E
}
}

@keyframes toast-enter {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes toast-exit {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-8px);
}
}

@layer utilities {
.toast-enter {
animation: toast-enter 300ms ease-out forwards;
}

.toast-exit {
animation: toast-exit 200ms ease-in forwards;
}
}

/* 자동완성이 안되는 문제가 있어 잠시 주석처리 합니다 */
/* @layer utilities {
/* typography */
Expand Down
7 changes: 3 additions & 4 deletions src/components/home/start-study-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import Image from 'next/image';
import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query';
import StartStudyModal from '@/features/study/participation/ui/start-study-modal';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';

export default function StartStudyButton() {
const { data: authData } = useAuth();
const memberId = authData?.memberId ?? null;
const isLoggedIn = !!memberId;
const { memberId, isAuthReady } = useAuthReady();
const isLoggedIn = isAuthReady && !!memberId;

const { data: userProfile } = useUserProfileQuery(memberId ?? 0);

Expand Down
24 changes: 16 additions & 8 deletions src/components/home/study-matching-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ import {
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
import StartStudyModal from '@/features/study/participation/ui/start-study-modal';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';

export default function StudyMatchingToggle() {
const { data: authData } = useAuth();
const memberId = authData?.memberId ?? null;
const isLoggedIn = !!memberId;
const { memberId, isAuthReady } = useAuthReady();
const isLoggedIn = isAuthReady && !!memberId;

const { data: userProfile } = useUserProfileQuery(memberId ?? 0);
const { mutate: patchAutoMatching, isPending } =
usePatchAutoMatchingMutation();

const { isVerified, setVerified } = usePhoneVerificationStatus(
memberId ?? undefined,
);
const {
isVerified,
isLoading: isVerificationLoading,
isError: isVerificationError,
setVerified,
} = usePhoneVerificationStatus(memberId ?? undefined);
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const [enabled, setEnabled] = useState(false);
Expand Down Expand Up @@ -60,6 +62,12 @@ export default function StudyMatchingToggle() {
};

const handleToggleChange = (checked: boolean) => {
if (isVerificationLoading) return;
if (isVerificationError) {
alert('인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.');

return;
}
// 토글을 켤 때만 본인인증 체크 (끌 때는 체크 안 함)
if (checked && !isVerified) {
setIsVerificationModalOpen(true);
Expand Down Expand Up @@ -105,7 +113,7 @@ export default function StudyMatchingToggle() {
size="md"
checked={enabled}
onCheckedChange={handleToggleChange}
disabled={isPending}
disabled={isPending || isVerificationLoading}
/>
</div>
</>
Expand Down
15 changes: 10 additions & 5 deletions src/components/home/tab-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useEffect, useState } from 'react';
import { getCookie } from '@/api/client/cookie';
import { cn } from '@/components/ui/(shadcn)/lib/utils';
import Button from '@/components/ui/button';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';
import { useScrollToHomeContent } from '@/hooks/use-scroll-to-home-content';

interface TabNavigationProps {
Expand Down Expand Up @@ -55,16 +55,21 @@ const TABS = [
export default function TabNavigation({ activeTab }: TabNavigationProps) {
const router = useRouter();
const searchParams = useSearchParams();
const { isAuthenticated } = useAuth();
const { isAuthReady, isHydrated, memberId } = useAuthReady();
const [canViewHistory, setCanViewHistory] = useState(false);
const visibleTabs = canViewHistory
? TABS
: TABS.filter((tab) => tab.id !== 'history');

useEffect(() => {
const hasMemberId = !!getCookie('memberId');
setCanViewHistory(isAuthenticated && hasMemberId);
}, [isAuthenticated]);
if (!isHydrated) {
setCanViewHistory(false);

return;
}
const hasMemberId = !!memberId || !!getCookie('memberId');
setCanViewHistory(isAuthReady && hasMemberId);
}, [isAuthReady, isHydrated, memberId]);

const scrollToHomeContent = useScrollToHomeContent();

Expand Down
6 changes: 3 additions & 3 deletions src/components/layout/sidebar/admin-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { usePathname } from 'next/navigation';
import UserAvatar from '@/components/ui/avatar';
import TabMenu from '@/components/ui/tab-menu';
import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';
import OutIcon from 'public/icons/out.svg';

export default function AdminSideBar() {
const { data: authData } = useAuth();
const { data: profile } = useUserProfileQuery(authData?.memberId ?? 0);
const { memberId } = useAuthReady();
const { data: profile } = useUserProfileQuery(memberId ?? 0);

const pathname = usePathname();

Expand Down
11 changes: 6 additions & 5 deletions src/components/lists/group-study-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { sendGTMEvent } from '@next/third-parties/google';
import Image from 'next/image';

import { GroupStudyListItemDto } from '@/api/openapi';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';
import { hashValue } from '@/utils/hash';

import StudyCard from '../card/study-card';
Expand All @@ -14,15 +14,16 @@ interface GroupStudyListProps {
}

export default function GroupStudyList({ studies }: GroupStudyListProps) {
const { data: authData } = useAuth();
const { memberId, isAuthReady } = useAuthReady();

const handleStudyClick = (study: GroupStudyListItemDto) => {
sendGTMEvent({
event: 'group_study_detail_view',
dl_timestamp: new Date().toISOString(),
...(authData?.memberId && {
dl_member_id: hashValue(String(authData.memberId)),
}),
...(isAuthReady &&
memberId && {
dl_member_id: hashValue(String(memberId)),
}),
dl_study_id: String(study.basicInfo?.groupStudyId),
dl_study_title: study.simpleDetailInfo?.title,
});
Expand Down
6 changes: 3 additions & 3 deletions src/components/lists/study-member-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Image from 'next/image';
import { useState } from 'react';
import { GetGroupStudyMemberStatusResponseContent } from '@/api/openapi';
import Pagination from '@/components/ui/pagination';
import { useAuth } from '@/hooks/common/use-auth';
import { useAuthReady } from '@/hooks/common/use-auth';
import { useGetGroupStudyMembers } from '@/hooks/queries/group-study-member-api';
import type { GroupStudyMember } from '../../features/study/group/api/group-study-types';

Expand Down Expand Up @@ -32,7 +32,7 @@ export default function StudyMemberList({
pageNumber: pageNumber,
pageSize: PAGE_SIZE,
});
const { data: authData } = useAuth();
const { memberId, isAuthReady } = useAuthReady();

if (isLoading) {
return null;
Expand All @@ -43,7 +43,7 @@ export default function StudyMemberList({

// memberList의 첫 번째 요소는 내 정보
const myInfo = memberList[0];
const isLeader = leaderId === authData?.memberId;
const isLeader = isAuthReady && leaderId === memberId;
const totalPages = Math.ceil((data?.totalMemberCount || 0) / PAGE_SIZE) || 1;

return (
Expand Down
Loading
Loading