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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@tosspayments/tosspayments-sdk": "^2.5.0",
"agentation": "^2.2.1",
"axios": "^1.9.0",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
Expand Down Expand Up @@ -97,6 +98,7 @@
"@tanstack/eslint-plugin-query": "^5.66.1",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^20",
"@types/qs": "^6.14.0",
"@types/react": "^19",
Expand Down
4 changes: 2 additions & 2 deletions src/app/(service)/(my)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export default async function MyLayout({
return (
<div className="flex h-full">
<Sidebar />
<div className="m-auto pt-500 pb-[100px]">
<div className="w-[780px]">{children}</div>
<div className="flex-1 px-400 pt-400 pb-[100px] lg:px-0 lg:pt-500">
<div className="w-full max-w-[780px] mx-auto">{children}</div>
Comment on lines +20 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check available spacing and width tokens in global.css

# Search for spacing tokens
rg -n "spacing|padding|--p-|--pb-" src/app/global.css

# Search for max-width tokens
rg -n "max-w|--max-w" src/app/global.css

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

Length of output: 3699


🏁 Script executed:

#!/bin/bash
# Check tailwind config to understand token naming and scale
fd -t f 'tailwind.config' . | head -5

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

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Look for any other pb (padding-bottom) token definitions
rg -n "pb-|padding-bottom" src/app/global.css | head -20

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

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Check if pb-400, pb-500 tokens exist
rg -n "@utility pb-" src/app/global.css

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

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Get the full context of lines 20-21 in the file
sed -n '15,25p' src/app/\(service\)/\(my\)/layout.tsx

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

Length of output: 364


🏁 Script executed:

#!/bin/bash
# Search more broadly for tailwind config files
find . -maxdepth 2 -name "tailwind.config.*" -o -name "tsconfig.json" | grep -E "(tailwind|ts)" | head -10

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

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Check if there's a CSS or config that might define pb-400, pb-500, etc. as utilities
rg -n "@utility.*p" src/app/global.css | grep -i "pb\|padding" | head -20

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

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Look at the complete spacing/utility definition section in global.css
sed -n '374,700p' src/app/global.css

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

Length of output: 6007


🏁 Script executed:

#!/bin/bash
# Check if there's a pb-400 or pb-500 utility defined elsewhere
rg -n "@utility pb-" src/app/global.css

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

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Look for any dynamic utility pattern or theme extension
grep -n "pb-\|padding-bottom" src/app/global.css | head -20

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

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Check if there are any other max-w definitions that might match 780px
rg -n "max-w-|@utility.*w" src/app/global.css | grep "max-w"

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

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Verify if this is an admin page (exempt from custom token rules)
head -5 src/app/\(service\)/\(my\)/layout.tsx

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

Length of output: 317


Tailwind 임의 값 사용이 코딩 가이드라인을 위반합니다.

pb-[100px]max-w-[780px]는 임의 값(arbitrary values)을 사용하고 있습니다. 프로젝트 커스텀 디자인 토큰만 사용해야 합니다. 예를 들어, px-400pt-400, pt-500은 올바르게 global.css에 정의된 spacing 토큰을 사용하고 있습니다. 비슷하게 pb-max-w-도 정의된 토큰 또는 새로운 utility 클래스를 사용해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(service)/(my)/layout.tsx around lines 20 - 21, The div in
layout.tsx is using Tailwind arbitrary values pb-[100px] and max-w-[780px],
which violates the design-token rule; replace pb-[100px] with an existing
spacing token class (e.g., pb-400) or add a new spacing token in global.css and
use that token's class, and replace max-w-[780px] with a custom max-width
utility defined in your design tokens (e.g., add a max-w-<token> in global.css
and use that class) so both pb and max-w use project-approved tokens instead of
arbitrary values.

</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
'use client';

import { useQueries } from '@tanstack/react-query';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import { useMemo, useState } from 'react';
import { axiosInstance } from '@/api/client/axios';
import GroupStudyReviewModal from '@/components/common/modals/group-study-review-modal';
import Pagination from '@/components/common/ui/pagination';
import { useAuthReady } from '@/hooks/common/use-auth';
import { useMemberStudyListQuery } from '@/hooks/queries/use-member-study-list-query';
import { useToastStore } from '@/stores/use-toast-store';
import type { MemberStudyItem } from '@/types/api/group-study.types';
import StudyReviewTabNav from './study-review-tab-nav';
import MemberStudyCard from '../group/_components/member-study-card';

const StudyCompletionModal = dynamic(
() => import('@/components/common/modals/study-completion-modal'),
{ ssr: false },
);

const StudyReviewModal = dynamic(
() => import('@/components/common/modals/study-review-modal'),
{ ssr: false },
);

interface CompletedStudyReviewPageProps {
basePath: string;
studyType: 'GROUP_STUDY' | 'PREMIUM_STUDY' | 'ONE_ON_ONE_STUDY';
studyTypeName: string;
hideTabNav?: boolean;
hideEmptyMessage?: boolean;
}

interface StudyRoleSectionProps {
title: string;
studies: MemberStudyItem[];
basePath: string;
emptyMessage: string;
onMemberClick?: (study: MemberStudyItem) => void;
}

function StudyRoleSection({
title,
studies,
basePath,
emptyMessage,
onMemberClick,
}: StudyRoleSectionProps) {
return (
<section className="flex flex-col gap-200">
<div className="flex items-center gap-100">
<h2 className="font-designer-20b text-text-default">{title}</h2>
</div>

{studies.length > 0 ? (
<ul className="grid grid-cols-1 gap-300 sm:grid-cols-2 lg:grid-cols-3">
{studies.map((study, index) => (
<MemberStudyCard
key={study.studyId ?? index}
study={study}
basePath={basePath}
onMemberClick={onMemberClick}
/>
))}
</ul>
) : (
<div className="font-designer-14r text-text-subtle flex h-200 items-center justify-center rounded-100 border border-border-subtle text-center">
{emptyMessage}
</div>
)}
</section>
);
}

export default function CompletedStudyReviewPage({
basePath,
studyType,
studyTypeName,
hideTabNav = false,
hideEmptyMessage = false,
}: CompletedStudyReviewPageProps) {
const [page, setPage] = useState(1);
const [reviewStudy, setReviewStudy] = useState<MemberStudyItem | null>(null);
const [submittedStudyIds, setSubmittedStudyIds] = useState<number[]>([]);
const [showCompletionModal, setShowCompletionModal] = useState(false);

const { memberId } = useAuthReady();
const showToast = useToastStore((state) => state.showToast);

const { data: completedStudyResponse } = useMemberStudyListQuery({
memberId: memberId ?? 0,
studyType,
studyStatus: 'COMPLETED',
completedPage: page,
completedPageSize: 6,
});

const completedStudies = useMemo(
() => completedStudyResponse?.completed.content ?? [],
[completedStudyResponse?.completed.content],
);
const participantStudies = completedStudies.filter(
(study) => study.studyRole === 'PARTICIPANT',
);
const leaderStudies = completedStudies.filter(
(study) => study.studyRole === 'LEADER',
);

const writtenResults = useQueries({
queries: participantStudies.map((study) => ({
queryKey: ['study-review', 'written', studyType, study.studyId],
queryFn: async () => {
const url =
studyType === 'ONE_ON_ONE_STUDY'
? `/study-spaces/${study.studyId}/reviews/written`
: `/group-studies/${study.studyId}/reviews/written`;
const { data } = await axiosInstance.get<{ content: boolean }>(url);

return data.content;
},
enabled: !!study.studyId,
staleTime: 60_000,
})),
});

const reviewWrittenByStudyId = new Map<number, boolean | undefined>();

participantStudies.forEach((study, index) => {
const writtenResult = writtenResults[index];
const isRecentlySubmitted = submittedStudyIds.includes(study.studyId);

reviewWrittenByStudyId.set(
study.studyId,
isRecentlySubmitted || writtenResult?.data === true
? true
: writtenResult?.data,
);
});

const handleParticipantStudyClick = (study: MemberStudyItem) => {
const reviewWritten = reviewWrittenByStudyId.get(study.studyId);

if (reviewWritten === true) {
showToast('이미 후기를 작성한 스터디입니다.', 'info');

return;
}

if (reviewWritten === undefined) {
showToast(
'후기 작성 가능 여부를 확인하는 중입니다. 잠시 후 다시 시도해주세요.',
'info',
);

return;
}

setReviewStudy(study);
};

const activeReviewStudyId = reviewStudy?.studyId;

const handleSubmitSuccess = () => {
if (activeReviewStudyId === undefined) return;
setSubmittedStudyIds((prev) =>
prev.includes(activeReviewStudyId)
? prev
: [...prev, activeReviewStudyId],
);
setTimeout(() => setShowCompletionModal(true), 300);
};

const emptyParticipatedMessage = `아직 참여한 ${studyTypeName}가 없습니다.`;
const emptyLedMessage = `아직 개설한 ${studyTypeName}가 없습니다.`;

return (
<div className="flex flex-col gap-400">
{!hideTabNav && <StudyReviewTabNav />}

{completedStudies.length === 0 ? (
!hideEmptyMessage && (
<div className="font-designer-14r text-text-subtle flex h-200 items-center justify-center text-center">
{emptyParticipatedMessage}
</div>
)
) : (
<>
<StudyRoleSection
title="참여한 스터디"
studies={participantStudies}
basePath={basePath}
emptyMessage={emptyParticipatedMessage}
onMemberClick={handleParticipantStudyClick}
/>

<StudyRoleSection
title="운영한 스터디"
studies={leaderStudies}
basePath={basePath}
emptyMessage={emptyLedMessage}
/>

<Pagination
page={page}
onChangePage={setPage}
totalPages={completedStudyResponse?.completed.totalPages ?? 1}
/>
</>
)}

{activeReviewStudyId !== undefined &&
reviewStudy &&
(studyType === 'ONE_ON_ONE_STUDY' ? (
<StudyReviewModal
open={!!reviewStudy}
onOpenChange={(open) => {
if (!open) {
setReviewStudy(null);
}
}}
targetStudySpaceId={activeReviewStudyId}
onSubmitSuccess={handleSubmitSuccess}
/>
) : (
<GroupStudyReviewModal
open={!!reviewStudy}
onOpenChange={(open) => {
if (!open) {
setReviewStudy(null);
}
}}
groupStudyId={activeReviewStudyId}
detailInfo={{ title: reviewStudy.title }}
basicInfo={{
startDate: dayjs(reviewStudy.startTime).format('YYYY.MM.DD'),
endDate: dayjs(reviewStudy.endTime).format('YYYY.MM.DD'),
}}
onSubmitSuccess={handleSubmitSuccess}
/>
))}

<StudyCompletionModal
open={showCompletionModal}
onOpenChange={setShowCompletionModal}
/>
</div>
);
}
Loading
Loading