Skip to content

Commit ebe6826

Browse files
role 뱃지 적용 및 빌더 피드 레이아웃 구조 수정
1 parent 1bca8c3 commit ebe6826

8 files changed

Lines changed: 158 additions & 58 deletions

File tree

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/page.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import Image from 'next/image';
1111
import Link from 'next/link';
1212
import { use, useState } from 'react';
13+
import { RoleBadge } from '@/app/(landing)/class/vibe-intro/_components/builder-feed-utils';
1314
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
1415
import MarkdownContentCore from '@/components/common/ui/rich-text/markdown-content-core';
1516
import { useAuth } from '@/features/auth/model/use-auth';
@@ -153,9 +154,14 @@ export default function FeedDetailPage({
153154
<p className="font-designer-14b text-gray-800">
154155
{feed?.author.nickname ?? ''}
155156
</p>
156-
<p className="font-designer-12r text-gray-400">
157-
{feed?.author.role ?? ''}
158-
</p>
157+
{feed?.author.role && (
158+
<RoleBadge
159+
role={feed.author.role}
160+
width={15}
161+
height={15}
162+
className="h-188 w-188"
163+
/>
164+
)}
159165
</div>
160166
</div>
161167

src/app/(landing)/class/vibe-intro/(learning)/feed/_components/feed-list-card.tsx

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Heart, MessageSquareMore } from 'lucide-react';
1+
import Image from 'next/image';
22
import Link from 'next/link';
33
import {
44
AuthorAvatar,
5+
ROLE_LABELS,
6+
RoleBadge,
57
formatRelativeTime,
68
} from '@/app/(landing)/class/vibe-intro/_components/builder-feed-utils';
79
import type { BuilderFeedListItemResponse } from '@/types/api/course.types';
@@ -10,43 +12,96 @@ function stripHtml(html: string): string {
1012
return html.replace(/<[^>]*>/g, '').trim();
1113
}
1214

13-
export function FeedListCard({
14-
feed,
15-
lessonLabel,
16-
}: {
17-
feed: BuilderFeedListItemResponse;
18-
lessonLabel: string;
19-
}) {
15+
export function FeedListCard({ feed }: { feed: BuilderFeedListItemResponse }) {
2016
return (
2117
<Link
2218
href={`/class/vibe-intro/feed/${feed.feedId}`}
23-
className="block border-b border-border-subtle py-300"
19+
className="flex flex-col overflow-hidden rounded-150 border border-border-subtle"
2420
>
25-
<p className="mb-125 font-designer-14r text-gray-500">{lessonLabel}</p>
26-
<p className="mb-300 line-clamp-2 font-designer-16r text-gray-800">
27-
{stripHtml(feed.content)}
28-
</p>
29-
<div className="flex items-center justify-between">
30-
<div className="flex items-center gap-150">
31-
<span className="flex items-center gap-75 font-designer-14r text-gray-500">
32-
<MessageSquareMore className="h-200 w-200" /> {feed.commentCount}
33-
</span>
34-
<span className="flex items-center gap-75 font-designer-14r text-gray-500">
35-
<Heart className="h-200 w-200" /> {feed.likeCount}
36-
</span>
37-
</div>
38-
<div className="flex items-center gap-100">
21+
{/* Profile + time */}
22+
<div className="flex items-center justify-between px-250 py-250">
23+
<div className="flex items-center gap-125">
3924
<AuthorAvatar
4025
nickname={feed.author.nickname}
41-
className="h-350 w-350"
26+
className="h-400 w-400"
27+
/>
28+
<div className="flex flex-col items-start justify-center">
29+
<div className="flex items-center gap-50">
30+
<p className="font-designer-14m text-gray-800">
31+
{feed.author.nickname}
32+
</p>
33+
<RoleBadge
34+
role={feed.author.role}
35+
width={15}
36+
height={15}
37+
className="h-188 w-188"
38+
/>
39+
</div>
40+
<p className="font-designer-12r text-gray-400">
41+
{ROLE_LABELS[feed.author.role] ?? feed.author.role}
42+
</p>
43+
</div>
44+
</div>
45+
<p className="font-designer-14r text-gray-400">
46+
{formatRelativeTime(feed.createdAt)}
47+
</p>
48+
</div>
49+
50+
{/* Thumbnail image */}
51+
<div className="relative aspect-square w-full bg-gray-200">
52+
{feed.thumbnailUrl && (
53+
<Image
54+
src={feed.thumbnailUrl}
55+
alt=""
56+
fill
57+
unoptimized
58+
className="object-cover"
4259
/>
43-
<p className="font-designer-14m text-gray-800">
44-
{feed.author.nickname}
45-
</p>
46-
<p className="font-designer-14r text-gray-400">
47-
{formatRelativeTime(feed.createdAt)}
48-
</p>
60+
)}
61+
</div>
62+
63+
{/* Actions + caption */}
64+
<div className="px-250 pb-250 pt-300">
65+
<div className="flex items-center gap-200">
66+
{/* Like */}
67+
<span className="flex items-center gap-50 font-designer-16r text-gray-800">
68+
<svg
69+
viewBox="0 0 24 24"
70+
className="h-300 w-300 shrink-0 text-background-brand-default"
71+
fill="currentColor"
72+
aria-hidden="true"
73+
>
74+
<path d="M16.5004 2.82495C14.7604 2.82495 13.0904 3.63495 12.0004 4.91495C10.9104 3.63495 9.24039 2.82495 7.50039 2.82495C4.42039 2.82495 2.00039 5.24495 2.00039 8.32495C2.00039 12.105 5.40039 15.185 10.5504 19.865L12.0004 21.175L13.4504 19.855C18.6004 15.185 22.0004 12.105 22.0004 8.32495C22.0004 5.24495 19.5804 2.82495 16.5004 2.82495ZM12.1004 18.375L12.0004 18.475L11.9004 18.375C7.14039 14.065 4.00039 11.215 4.00039 8.32495C4.00039 6.32495 5.50039 4.82495 7.50039 4.82495C9.04039 4.82495 10.5404 5.81495 11.0704 7.18495H12.9404C13.4604 5.81495 14.9604 4.82495 16.5004 4.82495C18.5004 4.82495 20.0004 6.32495 20.0004 8.32495C20.0004 11.215 16.8604 14.065 12.1004 18.375Z" />
75+
</svg>
76+
{feed.likeCount}
77+
</span>
78+
79+
{/* Comment */}
80+
<span className="flex items-center gap-50 font-designer-16r text-gray-800">
81+
<svg
82+
viewBox="0 0 20 20"
83+
className="h-300 w-300 shrink-0"
84+
fill="currentColor"
85+
aria-hidden="true"
86+
>
87+
<path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM18 14H4L2 16V2H18V14ZM5 7H7V9H5V7ZM9 7H11V9H9V7ZM13 7H15V9H13V7Z" />
88+
</svg>
89+
{feed.commentCount}
90+
</span>
91+
92+
{/* Share — reply arrow rotated 180° + flipped y = forward/share direction */}
93+
<svg
94+
viewBox="0 0 18 15"
95+
className="h-300 w-300 shrink-0 -scale-y-100 rotate-180"
96+
fill="currentColor"
97+
aria-hidden="true"
98+
>
99+
<path d="M7 4V0L0 7L7 14V9.9C12 9.9 15.5 11.5 18 15C17 10 14 5 7 4Z" />
100+
</svg>
49101
</div>
102+
<p className="mt-125 line-clamp-2 font-designer-14r text-gray-800">
103+
{stripHtml(feed.content)}
104+
</p>
50105
</div>
51106
</Link>
52107
);

src/app/(landing)/class/vibe-intro/(learning)/feed/page.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ export default function BuilderFeedPage() {
7373
[curriculum],
7474
);
7575

76-
const getLessonLabel = (id: number) =>
77-
lessonOptions.find((l) => l.lessonId === id)?.label ?? '레슨';
78-
7976
const lessonLabel =
8077
lessonOptions.find((l) => l.lessonId === lessonId)?.label ?? '전체';
8178

@@ -262,13 +259,9 @@ export default function BuilderFeedPage() {
262259
</p>
263260
</div>
264261
) : (
265-
<div className="flex flex-col">
262+
<div className="grid grid-cols-1 gap-300 sm:grid-cols-2 xl:grid-cols-3">
266263
{feeds.map((feed) => (
267-
<FeedListCard
268-
key={feed.feedId}
269-
feed={feed}
270-
lessonLabel={getLessonLabel(feed.lessonId)}
271-
/>
264+
<FeedListCard key={feed.feedId} feed={feed} />
272265
))}
273266
</div>
274267
)}

src/app/(landing)/class/vibe-intro/(learning)/qa/[id]/page.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Image from 'next/image';
1414
import Link from 'next/link';
1515
import { useRouter } from 'next/navigation';
1616
import { use, useRef, useState } from 'react';
17+
import { RoleBadge } from '@/app/(landing)/class/vibe-intro/_components/builder-feed-utils';
1718
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
1819
import MarkdownEditor from '@/components/common/ui/editor/markdown-editor';
1920
import { uploadCommunityMarkdownImage } from '@/features/community/model/community-markdown-image-upload';
@@ -31,15 +32,6 @@ import {
3132
import { useToastStore } from '@/stores/use-toast-store';
3233
import { analyzeError } from '@/utils/error-handler';
3334

34-
function GradeBadge({ role }: { role: string }) {
35-
const letter = role.charAt(0).toUpperCase();
36-
return (
37-
<div className="flex h-350 w-350 shrink-0 items-center justify-center rounded-full bg-rose-100 font-designer-14b text-rose-500">
38-
{letter}
39-
</div>
40-
);
41-
}
42-
4335
function formatDate(dateStr: string) {
4436
const d = new Date(dateStr);
4537
return `${d.getMonth() + 1}${d.getDate()}일`;
@@ -412,7 +404,12 @@ export default function QnaDetailPage({
412404
<p className="font-designer-14b text-gray-800">
413405
{qna.author.nickname}
414406
</p>
415-
<GradeBadge role={qna.author.role} />
407+
<RoleBadge
408+
role={qna.author.role}
409+
width={14}
410+
height={14}
411+
className="h-175 w-175"
412+
/>
416413
</div>
417414
<p className="font-designer-14r text-gray-400">
418415
{formatDate(qna.createdAt)} 작성
@@ -599,7 +596,12 @@ export default function QnaDetailPage({
599596
{answer.author.nickname.charAt(0)}
600597
</p>
601598
</div>
602-
<GradeBadge role={answer.author.role} />
599+
<RoleBadge
600+
role={answer.author.role}
601+
width={14}
602+
height={14}
603+
className="h-175 w-175"
604+
/>
603605
<div className="flex-1">
604606
<p className="font-designer-14b text-gray-800">
605607
{answer.author.nickname}

src/app/(landing)/class/vibe-intro/_components/builder-feed-utils.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import Image from 'next/image';
12
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
23

34
export const ROLE_LABELS: Record<string, string> = {
45
BUILDER: '빌더',
6+
MANAGER: '매니저',
57
ADMIN: '운영자',
68
};
79

10+
const ROLE_BADGE_SRC: Record<string, string> = {
11+
BUILDER: '/class/builder.png',
12+
MANAGER: '/class/manager.png',
13+
};
14+
815
export function formatRelativeTime(dateStr: string): string {
916
const minutes = Math.floor(
1017
(Date.now() - new Date(dateStr).getTime()) / 60000,
@@ -37,12 +44,38 @@ export function AuthorAvatar({
3744
);
3845
}
3946

47+
export function RoleBadge({
48+
role,
49+
width = 28,
50+
height = 28,
51+
className,
52+
}: {
53+
role: string;
54+
width?: number;
55+
height?: number;
56+
className?: string;
57+
}) {
58+
const src = ROLE_BADGE_SRC[role];
59+
if (!src) return null;
60+
return (
61+
<Image
62+
src={src}
63+
width={width}
64+
height={height}
65+
alt={ROLE_LABELS[role] ?? role}
66+
className={cn('shrink-0', className)}
67+
/>
68+
);
69+
}
70+
4071
export function BuilderBadge() {
4172
return (
42-
<div className="flex h-188 w-188 shrink-0 items-center justify-center rounded-full bg-[#6938ef]">
43-
<span className="text-[10px] font-semibold leading-none text-white">
44-
B
45-
</span>
46-
</div>
73+
<Image
74+
src="/class/builder.png"
75+
width={16}
76+
height={16}
77+
alt="빌더"
78+
className="h-188 w-188 shrink-0"
79+
/>
4780
);
4881
}

0 commit comments

Comments
 (0)