Skip to content

Commit 90b08e3

Browse files
Merge pull request #642 from code-zero-to-one/fix/class-detail
클래스/레슨 상세 UI 개선
2 parents 16ed19a + f052dbb commit 90b08e3

6 files changed

Lines changed: 125 additions & 53 deletions

File tree

src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-card.tsx

Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import {
77
ModeCommentIcon,
88
ModeEditIcon,
99
} from '@/components/common/ui/icons/course-icons';
10-
import type { LessonQnaSidebarItem } from '@/types/api/course.types';
10+
import type {
11+
BuilderQnaSidebarItem,
12+
LessonQnaSidebarItem,
13+
} from '@/types/api/course.types';
1114

1215
interface Props {
1316
myQnas: LessonQnaSidebarItem[];
17+
builderQnas: BuilderQnaSidebarItem[];
1418
onAskClick: () => void;
1519
onSelectQna: (qnaId: number) => void;
1620
}
@@ -21,10 +25,15 @@ function stripHtml(html: string): string {
2125

2226
function formatDate(dateStr: string) {
2327
const d = new Date(dateStr);
24-
return `${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
28+
return `${String(d.getFullYear()).slice(2)}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
2529
}
2630

27-
export function LessonQnaCard({ myQnas, onAskClick, onSelectQna }: Props) {
31+
export function LessonQnaCard({
32+
myQnas,
33+
builderQnas,
34+
onAskClick,
35+
onSelectQna,
36+
}: Props) {
2837
const [page, setPage] = useState(0);
2938
const total = myQnas.length;
3039
const hasMyQna = total > 0;
@@ -60,41 +69,58 @@ export function LessonQnaCard({ myQnas, onAskClick, onSelectQna }: Props) {
6069
</span>
6170
</div>
6271

63-
{current ? (
64-
<button
65-
type="button"
66-
onClick={() => onSelectQna(current.qnaId)}
67-
className="flex w-full items-center gap-125 rounded-100 border border-border-subtle bg-gray-100 p-150 text-left hover:border-border-brand"
68-
>
69-
<p className="flex-1 truncate font-designer-14b text-gray-800">
70-
{stripHtml(current.title)}
71-
</p>
72-
<div className="flex shrink-0 items-center gap-50 font-designer-13m text-gray-400">
73-
<ModeCommentIcon className="h-225 w-225" />
74-
<span>{current.answerCount}</span>
75-
</div>
76-
<p className="shrink-0 font-designer-13m text-gray-400">
77-
{formatDate(current.createdAt)}
78-
</p>
79-
</button>
80-
) : (
81-
<p className="rounded-100 border border-border-subtle bg-gray-100 p-150 text-center font-designer-13m text-gray-400">
82-
아직 질문이 없어요.
83-
</p>
84-
)}
72+
<div className="flex flex-col gap-100">
73+
<div className="relative">
74+
{hasMyQna && (
75+
<button
76+
type="button"
77+
onClick={() => setPage((p) => Math.max(0, p - 1))}
78+
disabled={page === 0}
79+
aria-label="이전 질문"
80+
className="absolute -left-300 top-1/2 -translate-y-1/2 text-border-default disabled:opacity-50"
81+
>
82+
<ChevronLeft className="h-300 w-300" />
83+
</button>
84+
)}
85+
86+
{current ? (
87+
<button
88+
type="button"
89+
onClick={() => onSelectQna(current.qnaId)}
90+
className="flex w-full items-center gap-125 rounded-100 border border-border-subtle bg-gray-100 p-150 text-left hover:border-border-brand"
91+
>
92+
<p className="flex-1 truncate font-designer-14b text-gray-800">
93+
{stripHtml(current.title)}
94+
</p>
95+
<div className="flex shrink-0 items-center gap-50 font-designer-13m text-gray-400">
96+
<ModeCommentIcon className="h-225 w-225" />
97+
<p className="shrink-0 font-designer-13m text-gray-400">
98+
{formatDate(current.createdAt)}
99+
</p>
100+
<span>{current.answerCount}</span>
101+
</div>
102+
</button>
103+
) : (
104+
<p className="rounded-100 border border-border-subtle bg-gray-100 p-150 text-center font-designer-13m text-gray-400">
105+
아직 질문이 없어요.
106+
</p>
107+
)}
108+
109+
{hasMyQna && (
110+
<button
111+
type="button"
112+
onClick={() => setPage((p) => Math.min(total - 1, p + 1))}
113+
disabled={page >= total - 1}
114+
aria-label="다음 질문"
115+
className="absolute -right-300 top-1/2 -translate-y-1/2 text-border-default disabled:opacity-50"
116+
>
117+
<ChevronRight className="h-300 w-300" />
118+
</button>
119+
)}
120+
</div>
85121

86-
{hasMyQna && (
87-
<div className="flex items-center justify-between">
88-
<button
89-
type="button"
90-
onClick={() => setPage((p) => Math.max(0, p - 1))}
91-
disabled={page === 0}
92-
aria-label="이전 질문"
93-
className="text-border-default disabled:opacity-50"
94-
>
95-
<ChevronLeft className="h-300 w-300" />
96-
</button>
97-
<div className="flex gap-50">
122+
{hasMyQna && total > 1 && (
123+
<div className="flex justify-center gap-50">
98124
{myQnas.map((q, i) => (
99125
<span
100126
key={q.qnaId}
@@ -105,16 +131,48 @@ export function LessonQnaCard({ myQnas, onAskClick, onSelectQna }: Props) {
105131
/>
106132
))}
107133
</div>
108-
<button
109-
type="button"
110-
onClick={() => setPage((p) => Math.min(total - 1, p + 1))}
111-
disabled={page >= total - 1}
112-
aria-label="다음 질문"
113-
className="text-border-default disabled:opacity-50"
114-
>
115-
<ChevronRight className="h-300 w-300" />
116-
</button>
134+
)}
135+
</div>
136+
</div>
137+
138+
<div className="flex flex-col gap-150">
139+
<p className="font-designer-14b text-gray-1000">빌더들의 질문</p>
140+
141+
{builderQnas.length > 0 ? (
142+
<div className="flex flex-col gap-125">
143+
{builderQnas.map((q, i) => (
144+
<button
145+
key={q.qnaId ?? i}
146+
type="button"
147+
onClick={() => onSelectQna(q.qnaId)}
148+
className="flex h-1250 w-full flex-col gap-50 overflow-hidden rounded-100 border border-gray-200 bg-gray-100 px-150 py-175 text-left hover:border-border-brand"
149+
>
150+
<div className="flex w-full items-center gap-125">
151+
<p className="flex-1 truncate font-designer-14b text-gray-800">
152+
{stripHtml(q.title)}
153+
</p>
154+
<p className="shrink-0 font-designer-13m text-gray-400">
155+
{formatDate(q.createdAt)}
156+
</p>
157+
<div className="flex shrink-0 items-center gap-50">
158+
<ModeCommentIcon className="h-225 w-225 text-gray-400" />
159+
<span className="font-designer-13m text-gray-400">
160+
{q.answerCount}
161+
</span>
162+
</div>
163+
</div>
164+
{q.preview && (
165+
<p className="line-clamp-2 font-designer-14r text-gray-1000">
166+
{q.preview}
167+
</p>
168+
)}
169+
</button>
170+
))}
117171
</div>
172+
) : (
173+
<p className="rounded-100 border border-border-subtle bg-gray-100 p-150 text-center font-designer-13m text-gray-400">
174+
아직 질문이 없어요.
175+
</p>
118176
)}
119177
</div>
120178
</div>

src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-top-bar.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ export function LessonTopBar({
4545
</button>
4646

4747
<div className="flex items-center gap-350 px-350">
48+
<p className="font-designer-16b text-gray-1000">{courseTitle}</p>
4849
<div className="flex items-center rounded-100 bg-rose-200 px-125 py-25">
4950
<p className="font-designer-16m text-rose-400">
5051
{currentLesson} / {totalLessons}
5152
</p>
5253
</div>
53-
<p className="font-designer-16b text-gray-1000">{courseTitle}</p>
5454
</div>
5555

5656
<div className="flex-1 px-300">
@@ -65,9 +65,7 @@ export function LessonTopBar({
6565
<div className="mr-400 flex h-450 items-center rounded-100 bg-gray-800 px-250">
6666
<p className="font-designer-16b">
6767
<span className="text-text-brand">{discordCount}</span>
68-
<span className="text-text-inverse">
69-
명과 디스코드에서 함께 공부중!
70-
</span>
68+
<span className="text-text-inverse">명과 함께 공부 중!</span>
7169
</p>
7270
</div>
7371
</div>

src/app/(class-lesson)/class/[slug]/lesson/[id]/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ArrowLeft } from 'lucide-react';
44
import Link from 'next/link';
55
import { useRouter } from 'next/navigation';
66
import { use, useEffect, useMemo, useRef, useState } from 'react';
7+
import FloatingClassActionButtons from '@/components/common/ui/floating-class-action-buttons';
78
import MarkdownContentCore from '@/components/common/ui/rich-text/markdown-content-core';
89
import {
910
useGetCourseDrawer,
@@ -207,7 +208,7 @@ export default function LessonPage({
207208
</p>
208209
) : null}
209210

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

@@ -244,9 +245,10 @@ export default function LessonPage({
244245
</div>
245246

246247
{/* RIGHT sticky sidebar */}
247-
<div className="sticky top-[88px] flex flex-col gap-250">
248+
<div className="sticky top-800 z-10 flex flex-col gap-250">
248249
<LessonQnaCard
249250
myQnas={qnaSidebar?.qnas ?? []}
251+
builderQnas={qnaSidebar?.builderQnas ?? []}
250252
onAskClick={() => setSubmissionModalOpen(true)}
251253
onSelectQna={setSelectedQnaId}
252254
/>
@@ -272,6 +274,7 @@ export default function LessonPage({
272274
feedId={selectedFeedId}
273275
onClose={() => setSelectedFeedId(null)}
274276
/>
277+
<FloatingClassActionButtons />
275278
</div>
276279
);
277280
}

src/app/(landing)/class/[slug]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Flame, History, ThumbsUp } from 'lucide-react';
44
import { useRouter } from 'next/navigation';
55
import { use, useMemo, useState } from 'react';
6+
import FloatingClassActionButtons from '@/components/common/ui/floating-class-action-buttons';
67
import { ClassDetailBenefitsSection } from '@/components/pages/class/class-detail-benefits-section';
78
import { ClassDetailBuilderFeedSection } from '@/components/pages/class/class-detail-builder-feed-section';
89
import {
@@ -273,6 +274,7 @@ export default function ClassDetailPage({
273274
/>
274275
</div>
275276
</div>
277+
<FloatingClassActionButtons />
276278
</div>
277279
);
278280
}

src/app/(landing)/class/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ function CourseCard({
467467
<span className="font-designer-16b text-text-brand">
468468
{course.learnerCount}
469469
</span>
470-
{course.learnerLabel}
470+
{course.learnerLabel.replace(/^\d+/, '')}
471471
</p>
472472
</div>
473473

src/types/api/course.types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,19 @@ export interface LessonQnaSidebarItem {
649649
createdAt: string;
650650
}
651651

652+
// TODO: backend to add `builderQnas: List<BuilderQna>` to LessonQnaSidebarResponse.java
653+
// BuilderQna should include: qnaId, title, answerCount, createdAt, preview (content excerpt)
654+
export interface BuilderQnaSidebarItem {
655+
qnaId: number;
656+
title: string;
657+
answerCount: number;
658+
createdAt: string;
659+
preview?: string;
660+
}
661+
652662
export interface LessonQnaSidebarResponse {
653663
qnas: LessonQnaSidebarItem[];
664+
builderQnas?: BuilderQnaSidebarItem[];
654665
}
655666

656667
// ─── Gift Email ───────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)