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: 4 additions & 3 deletions e2e/class/journey-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ test.beforeEach(async ({ context, baseURL }) => {
function makeCourseDetail(overrides: Partial<CourseDetailResponse> = {}): {
content: CourseDetailResponse;
} {
const viewerStatus = overrides.viewerStatus ?? 'FREE_ENROLLED';
const base: CourseDetailResponse = {
courseId: COURSE_ID,
slug: 'vibe-intro',
Expand All @@ -86,11 +87,11 @@ function makeCourseDetail(overrides: Partial<CourseDetailResponse> = {}): {
],
earlyBirdEndsAt: null,
canFreeEnroll: null,
isFreeEnrolled: true,
isFreeEnrolled: viewerStatus === 'FREE_ENROLLED',
freeLessonCount: 3,
journeyMapAvailable: true,
hasFullAccess: false,
isPaidEnrolled: false,
hasFullAccess: viewerStatus === 'PAID',
isPaidEnrolled: viewerStatus === 'PAID',
canPurchase: true,
};
return { content: { ...base, ...overrides } };
Expand Down
172 changes: 90 additions & 82 deletions src/app/(class-lesson)/class/[slug]/lesson/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ export default function LessonPage({
const reviewRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const topBarRef = useRef<HTMLDivElement>(null);
const leftColRef = useRef<HTMLDivElement>(null);

function scrollToRef(ref: RefObject<HTMLDivElement | null>) {
if (!ref.current) return;
const headerHeight = topBarRef.current?.offsetHeight ?? 64;
if (!ref.current || !leftColRef.current) return;
const container = leftColRef.current;
const top =
ref.current.getBoundingClientRect().top +
window.scrollY -
headerHeight -
16;
window.scrollTo({ top, behavior: 'smooth' });
container.scrollTop +
ref.current.getBoundingClientRect().top -
container.getBoundingClientRect().top;
container.scrollTo({ top, behavior: 'smooth' });
}

function handleTabChange(next: LessonTabValue) {
Expand Down Expand Up @@ -200,93 +200,101 @@ export default function LessonPage({
</div>

<div className="mx-auto w-full max-w-[1236px] px-300">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

금지된 Tailwind arbitrary 값을 토큰 클래스로 교체해 주세요.

Line 202(max-w-[1236px]), Line 205(h-[calc(100vh-var(--spacing-800))]), Line 249(min-h-[964px])는 현재 경로 규칙에서 허용되지 않습니다. global.css에 정의된 프로젝트 토큰 클래스로 변경이 필요합니다.

As per coding guidelines "src/app/**/*.{tsx,jsx}: Do not use arbitrary Tailwind values in className attributes... Use only defined tokens from global.css" and "src/**/*.{ts,tsx,css}: No Tailwind arbitrary values ... use project custom tokens from global.css".

Also applies to: 205-205, 249-249

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(class-lesson)/class/[slug]/lesson/[id]/page.tsx at line 202,
Replace the prohibited arbitrary Tailwind classes in the JSX elements that
contain "max-w-[1236px]", "h-[calc(100vh-var(--spacing-800))]" and
"min-h-[964px]" with the project token classes defined in global.css: swap
max-w-[1236px] for the corresponding max-width token (e.g., max-w-{your-token}),
replace the calc height expression with a height token that composes the
--spacing-800 token (e.g., h-{viewport-minus-spacing-token} or a utility that
uses var(--spacing-800)), and replace min-h-[964px] with the matching min-height
token from global.css; update the className strings in the same JSX elements
(the div with max-w-[1236px] and the elements containing the h- and min-h-
arbitraries) to use those token class names so no arbitrary Tailwind values
remain.

<div className="grid grid-cols-content-sidebar-360 items-start gap-250 pt-500">
<div className="flex items-start gap-250">
{/* LEFT */}
<div className="min-w-0">
<Link
href={`/class/${lesson?.courseSlug ?? slug}/home`}
className="inline-flex items-center gap-125 rounded-full border border-gray-200 bg-background-default px-200 py-100"
>
<ArrowLeft className="h-250 w-250 text-gray-800" />
<span className="font-designer-14m text-gray-1000">
학습 여정 맵 돌아가기
</span>
</Link>

<div className="mt-300 flex items-center justify-between">
<div className="flex items-center gap-200">
<span className="rounded-100 bg-rose-200 px-125 py-25 font-designer-14m text-rose-400">
Lesson {String(lessonId).padStart(2, '0')}
<div className="min-w-0 flex-1 flex flex-col h-[calc(100vh-var(--spacing-800))]">
{/* Fixed header: back link, title, description, tabs — never scrolls */}
<div className="shrink-0 pt-500">
<Link
href={`/class/${lesson?.courseSlug ?? slug}/home`}
className="inline-flex items-center gap-125 rounded-full border border-gray-200 bg-background-default px-200 py-100"
>
<ArrowLeft className="h-250 w-250 text-gray-800" />
<span className="font-designer-14m text-gray-1000">
학습 여정 맵 돌아가기
</span>
<h1 className="font-designer-32b text-gray-800">
{lesson?.title ?? 'AI 처음 만나는 날'}
</h1>
</Link>

<div className="mt-300 flex items-center justify-between">
<div className="flex items-center gap-200">
<span className="rounded-100 bg-rose-200 px-125 py-25 font-designer-14m text-rose-400">
Lesson {String(lessonId).padStart(2, '0')}
</span>
<h1 className="font-designer-32b text-gray-800">
{lesson?.title ?? 'AI 처음 만나는 날'}
</h1>
</div>
<p className="font-designer-16m text-gray-500">
{lesson?.estimatedMinutes
? `약 ${lesson.estimatedMinutes}분 소요`
: ''}
</p>
</div>
<p className="font-designer-16m text-gray-500">
{lesson?.estimatedMinutes
? `약 ${lesson.estimatedMinutes}분 소요`
: ''}
</p>
</div>

{lesson?.description ? (
<p className="mt-150 whitespace-pre-line font-designer-16r text-gray-700">
{lesson.description}
</p>
) : null}
{lesson?.description ? (
<p className="mt-150 whitespace-pre-line font-designer-16r text-gray-700">
{lesson.description}
</p>
) : null}

<div className="sticky top-800 z-20 mt-300 bg-gray-100">
<LessonTabs value={tab} onChange={handleTabChange} />
<div className="mt-300 bg-gray-100">
<LessonTabs value={tab} onChange={handleTabChange} />
</div>
</div>

<div
ref={contentRef}
className="mt-300 min-h-[964px] rounded-150 bg-background-default p-500"
>
{lesson?.contentMarkdown ? (
<MarkdownContentCore content={lesson.contentMarkdown} />
) : (
<p className="font-designer-16r text-gray-500">
본문이 준비 중입니다.
</p>
)}
</div>
{/* Scroll area: content + review form only */}
<div ref={leftColRef} className="flex-1 overflow-y-auto">
<div
ref={contentRef}
className="mt-300 min-h-[964px] rounded-150 bg-background-default p-500"
>
{lesson?.contentMarkdown ? (
<MarkdownContentCore content={lesson.contentMarkdown} />
) : (
<p className="font-designer-16r text-gray-500">
본문이 준비 중입니다.
</p>
)}
</div>

<div ref={reviewRef} />
<hr className="my-500 border-gray-300" />
<div ref={reviewRef} />
<hr className="my-500 border-gray-300" />

{tab === 'review' || tab === 'follow' ? (
<LessonReviewForm
key={lessonId}
retrospectivePurpose={
lesson?.retrospectivePurpose ?? 'ARTIFACT_SHARE'
}
retrospectivePrompt={lesson?.retrospectivePrompt}
artifactSubmissionRequired={
lesson?.artifactSubmissionRequired ?? false
}
alreadySubmitted={alreadySubmitted}
submitting={submitRetrospective.isPending}
isLastLesson={isLastLesson}
onSubmit={handleSubmit}
/>
) : null}
{tab === 'review' || tab === 'follow' ? (
<LessonReviewForm
key={lessonId}
retrospectivePurpose={
lesson?.retrospectivePurpose ?? 'ARTIFACT_SHARE'
}
retrospectivePrompt={lesson?.retrospectivePrompt}
artifactSubmissionRequired={
lesson?.artifactSubmissionRequired ?? false
}
alreadySubmitted={alreadySubmitted}
submitting={submitRetrospective.isPending}
isLastLesson={isLastLesson}
onSubmit={handleSubmit}
/>
) : null}

<div className="h-1000" />
<div className="h-1000" />
</div>
</div>

{/* RIGHT sticky sidebar */}
<div className="sticky top-800 z-10 flex flex-col gap-250">
<LessonQnaCard
myQnas={qnaSidebar?.qnas ?? []}
builderQnas={qnaSidebar?.builderQnas ?? []}
onAskClick={() => setSubmissionModalOpen(true)}
onSelectQna={setSelectedQnaId}
/>
<LessonBuilderFeedCard
feeds={feedPreview?.feeds ?? []}
onSelectFeed={setSelectedFeedId}
/>
{/* RIGHT sidebar — stays put while left column scrolls */}
<div className="w-4500 shrink-0 pt-500">
<div className="flex flex-col gap-250">
<LessonQnaCard
myQnas={qnaSidebar?.qnas ?? []}
builderQnas={qnaSidebar?.builderQnas ?? []}
onAskClick={() => setSubmissionModalOpen(true)}
onSelectQna={setSelectedQnaId}
/>
<LessonBuilderFeedCard
feeds={feedPreview?.feeds ?? []}
onSelectFeed={setSelectedFeedId}
/>
</div>
</div>
</div>
</div>
Expand Down
Loading