feat: pretty gallery info
This commit is contained in:
		
							parent
							
								
									c26c3f7235
								
							
						
					
					
						commit
						f8e2b43e79
					
				
					 2 changed files with 172 additions and 81 deletions
				
			
		| 
						 | 
					@ -2,29 +2,47 @@ import StyledLink from "@/components/gallery/StyledLink";
 | 
				
			||||||
import { cn } from "@/lib/utils";
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
import { Fragment } from "react/jsx-runtime";
 | 
					import { Fragment } from "react/jsx-runtime";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DescItem({ name, children, className }: {
 | 
					
 | 
				
			||||||
 | 
					export function DescItem({ 
 | 
				
			||||||
 | 
					    name, 
 | 
				
			||||||
 | 
					    children, 
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    icon 
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
    className?: string;
 | 
					    className?: string;
 | 
				
			||||||
    children?: React.ReactNode;
 | 
					    children?: React.ReactNode;
 | 
				
			||||||
 | 
					    icon?: React.ReactNode;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
    return <div className={cn("grid content-start", className)}>
 | 
					    return <div className={cn("grid content-start", className)}>
 | 
				
			||||||
        <span className="text-muted-foreground text-sm">{name}</span>
 | 
					        <div className="flex items-center gap-1.5 mb-1">
 | 
				
			||||||
        <span className="text-primary leading-4 font-medium">{children}</span>
 | 
					            {icon && <span className="text-muted-foreground">{icon}</span>}
 | 
				
			||||||
 | 
					            <span className="text-muted-foreground text-sm font-medium">{name}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className="text-primary leading-relaxed font-medium pl-0.5">{children}</div>
 | 
				
			||||||
    </div>;
 | 
					    </div>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DescTagItem({
 | 
					export function DescTagItem({
 | 
				
			||||||
    items, name, className,
 | 
					    items, name, className, icon
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
    items: string[];
 | 
					    items: string[];
 | 
				
			||||||
    className?: string;
 | 
					    className?: string;
 | 
				
			||||||
 | 
					    icon?: React.ReactNode;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
    return <DescItem name={name} className={className}>
 | 
					    return <DescItem name={name} className={className} icon={icon}>
 | 
				
			||||||
        {items.length === 0 ? "N/A" : items.map(
 | 
					        {items.length === 0 ? (
 | 
				
			||||||
 | 
					            <span className="text-muted-foreground italic text-sm">정보 없음</span>
 | 
				
			||||||
 | 
					        ) : items.map(
 | 
				
			||||||
            (x, i) => 
 | 
					            (x, i) => 
 | 
				
			||||||
            <Fragment key={x}>
 | 
					            <Fragment key={x}>
 | 
				
			||||||
                <StyledLink to={`/search?allow_tag=${name.toLowerCase()}:${x}`}>{x}</StyledLink>
 | 
					                <StyledLink 
 | 
				
			||||||
                {i + 1 < items.length && <span className="">, </span>}
 | 
					                    to={`/search?allow_tag=${name.toLowerCase()}:${x}`}
 | 
				
			||||||
 | 
					                    className="inline-flex items-center hover:bg-secondary/40 px-1.5 py-0.5 rounded-md transition-colors">
 | 
				
			||||||
 | 
					                    {x}
 | 
				
			||||||
 | 
					                </StyledLink>
 | 
				
			||||||
 | 
					                {i + 1 < items.length && <span className="mx-0.5">, </span>}
 | 
				
			||||||
            </Fragment>
 | 
					            </Fragment>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
    </DescItem>;
 | 
					    </DescItem>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,12 @@ import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
 | 
				
			||||||
import { useEffect, useRef } from "react";
 | 
					import { useEffect, useRef } from "react";
 | 
				
			||||||
import { useLogin } from "@/state/user.ts";
 | 
					import { useLogin } from "@/state/user.ts";
 | 
				
			||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu.tsx";
 | 
					import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu.tsx";
 | 
				
			||||||
import { DotsVerticalIcon } from "@radix-ui/react-icons";
 | 
					import { EllipsisVerticalIcon } from "lucide-react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    RefreshCw, ScanSearch, Trash2,
 | 
				
			||||||
 | 
					    Paintbrush
 | 
				
			||||||
 | 
					    , Users, BookOpen, User, Clock, FileCheck, FileText, Layers, Tag, Loader2, AlertCircle, FileQuestion
 | 
				
			||||||
 | 
					} from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ContentInfoPageProps {
 | 
					export interface ContentInfoPageProps {
 | 
				
			||||||
    params: {
 | 
					    params: {
 | 
				
			||||||
| 
						 | 
					@ -45,19 +50,30 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
 | 
				
			||||||
    const isAdmin = username === "admin";
 | 
					    const isAdmin = username === "admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isLoading) {
 | 
					    if (isLoading) {
 | 
				
			||||||
        return <div className="p-4 flex items-center justify-center h-full">
 | 
					        return <div className="p-8 flex flex-col items-center justify-center h-full">
 | 
				
			||||||
            <span className="animate-pulse text-4xl">
 | 
					            <Loader2 className="w-16 h-16 animate-spin text-primary mb-4" />
 | 
				
			||||||
                Loading...
 | 
					            <span className="text-xl font-medium text-muted-foreground">
 | 
				
			||||||
 | 
					                콘텐츠 정보 로드 중...
 | 
				
			||||||
            </span>
 | 
					            </span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (error) {
 | 
					    if (error) {
 | 
				
			||||||
        return <div className="p-4">Error: {String(error)}</div>
 | 
					        return <div className="p-8 flex flex-col items-center justify-center h-full text-destructive">
 | 
				
			||||||
 | 
					            <AlertCircle className="w-16 h-16 mb-4" />
 | 
				
			||||||
 | 
					            <div className="text-xl font-medium mb-2">오류가 발생했습니다</div>
 | 
				
			||||||
 | 
					            <div className="text-destructive/80 bg-destructive/10 p-3 rounded-md">{String(error)}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!data) {
 | 
					    if (!data) {
 | 
				
			||||||
        return <div className="p-4">Not found</div>
 | 
					        return <div className="p-8 flex flex-col items-center justify-center h-full text-muted-foreground">
 | 
				
			||||||
 | 
					            <FileQuestion className="w-16 h-16 mb-4" />
 | 
				
			||||||
 | 
					            <div className="text-xl font-medium">콘텐츠를 찾을 수 없습니다</div>
 | 
				
			||||||
 | 
					            <Button variant="outline" className="mt-4">
 | 
				
			||||||
 | 
					                <Link href="/search">검색 페이지로 이동</Link>
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const tags = data?.tags ?? [];
 | 
					    const tags = data?.tags ?? [];
 | 
				
			||||||
| 
						 | 
					@ -68,77 +84,117 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <Wrapper>
 | 
					        <Wrapper>
 | 
				
			||||||
            <Link to={contentLocation}>
 | 
					            <Link to={contentLocation}>
 | 
				
			||||||
                <div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
 | 
					                <div className="m-auto h-[400px] mb-6 flex justify-center items-center flex-none bg-gradient-to-b from-[#1d1d25] to-[#272733]
 | 
				
			||||||
            rounded-xl shadow-lg overflow-hidden">
 | 
					            rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 group">
 | 
				
			||||||
                    <img
 | 
					                    <img
 | 
				
			||||||
                        className="max-w-full max-h-full object-cover object-center"
 | 
					                        className="max-w-full max-h-full object-cover object-center group-hover:scale-[1.02] transition-transform duration-300"
 | 
				
			||||||
                        src={`/api/doc/${data.id}/comic/thumbnail`}
 | 
					                        src={`/api/doc/${data.id}/comic/thumbnail`}
 | 
				
			||||||
                        alt={data.title} />
 | 
					                        alt={data.title} />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </Link>
 | 
					            </Link>
 | 
				
			||||||
            <Card className="flex-1 relative">
 | 
					            <Card className="flex-1 relative">
 | 
				
			||||||
                {isAdmin &&
 | 
					                <div className="flex justify-between items-start p-6">
 | 
				
			||||||
                    <div className="absolute top-0 right-0 p-2">
 | 
					                    <CardHeader className="p-0">
 | 
				
			||||||
                        <DropdownMenu>
 | 
					                        <CardTitle className="font-bold bg-clip-text">
 | 
				
			||||||
                            <DropdownMenuTrigger asChild>
 | 
					                            <StyledLink to={contentLocation} className="hover:no-underline">
 | 
				
			||||||
                                <Button variant="ghost"> <DotsVerticalIcon /> Actions</Button>
 | 
					                                {data.title}
 | 
				
			||||||
                            </DropdownMenuTrigger>
 | 
					                            </StyledLink>
 | 
				
			||||||
                            <DropdownMenuContent align="end" >
 | 
					                        </CardTitle>
 | 
				
			||||||
                                <DropdownMenuItem
 | 
					                        <CardDescription className="mt-1">
 | 
				
			||||||
                                    onClick={async () => {
 | 
					                            <StyledLink to={`/search?allow_tag=type:${classifiedTags.type[0] ?? ""}`}
 | 
				
			||||||
                                        await rehashDoc(params.id);
 | 
					                                className="text-sm px-2 py-0.5 rounded-md inline-flex items-center gap-1">
 | 
				
			||||||
                                    }}
 | 
					                                <Tag className="w-3.5 h-3.5" />
 | 
				
			||||||
 | 
					                                {classifiedTags.type[0] ?? "N/A"}
 | 
				
			||||||
 | 
					                            </StyledLink>
 | 
				
			||||||
 | 
					                        </CardDescription>
 | 
				
			||||||
 | 
					                    </CardHeader>
 | 
				
			||||||
 | 
					                    {isAdmin &&
 | 
				
			||||||
 | 
					                        <div className="z-10">
 | 
				
			||||||
 | 
					                            <DropdownMenu>
 | 
				
			||||||
 | 
					                                <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					                                    <Button variant="ghost" className="hover:bg-secondary/50 flex items-center gap-1.5 rounded-full px-3">
 | 
				
			||||||
 | 
					                                        <EllipsisVerticalIcon className="w-4 h-4" />
 | 
				
			||||||
 | 
					                                        <span className="font-medium">Actions</span>
 | 
				
			||||||
 | 
					                                    </Button>
 | 
				
			||||||
 | 
					                                </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					                                <DropdownMenuContent align="end" className="animate-in fade-in-50 zoom-in-95 duration-100">
 | 
				
			||||||
 | 
					                                    <DropdownMenuItem
 | 
				
			||||||
 | 
					                                        onClick={async () => {
 | 
				
			||||||
 | 
					                                            await rehashDoc(params.id);
 | 
				
			||||||
 | 
					                                        }}
 | 
				
			||||||
 | 
					                                        className="flex items-center gap-2 cursor-pointer"
 | 
				
			||||||
                                    >
 | 
					                                    >
 | 
				
			||||||
                                    Rehash
 | 
					                                        <RefreshCw className="w-4 h-4" />
 | 
				
			||||||
                                </DropdownMenuItem>
 | 
					                                        Rehash
 | 
				
			||||||
                                <DropdownMenuItem
 | 
					                                    </DropdownMenuItem>
 | 
				
			||||||
                                    onClick={async () => {
 | 
					                                    <DropdownMenuItem
 | 
				
			||||||
                                        if (!window.confirm("Are you sure?")) {
 | 
					                                        onClick={async () => {
 | 
				
			||||||
                                            return;
 | 
					                                            if (!window.confirm("Are you sure?")) {
 | 
				
			||||||
                                        }
 | 
					                                                return;
 | 
				
			||||||
                                        // Rescan
 | 
					                                            }
 | 
				
			||||||
                                        await rescanDoc(params.id);
 | 
					                                            await rescanDoc(params.id);
 | 
				
			||||||
                                    }}
 | 
					                                        }}
 | 
				
			||||||
 | 
					                                        className="flex items-center gap-2 cursor-pointer"
 | 
				
			||||||
                                    >
 | 
					                                    >
 | 
				
			||||||
                                    Rescan
 | 
					                                        <ScanSearch className="w-4 h-4" />
 | 
				
			||||||
                                </DropdownMenuItem>
 | 
					                                        Rescan
 | 
				
			||||||
                                <DropdownMenuItem
 | 
					                                    </DropdownMenuItem>
 | 
				
			||||||
                                    onClick={async () => {
 | 
					                                    <DropdownMenuItem
 | 
				
			||||||
                                        if (!window.confirm("Are you sure?")) {
 | 
					                                        onClick={async () => {
 | 
				
			||||||
                                            return;
 | 
					                                            if (!window.confirm("Are you sure?")) {
 | 
				
			||||||
                                        }
 | 
					                                                return;
 | 
				
			||||||
                                        // Delete
 | 
					                                            }
 | 
				
			||||||
                                        await deleteDoc(params.id);
 | 
					                                            await deleteDoc(params.id);
 | 
				
			||||||
                                    }}
 | 
					                                        }}
 | 
				
			||||||
 | 
					                                        className="flex items-center gap-2 cursor-pointer text-destructive hover:text-destructive"
 | 
				
			||||||
                                    >
 | 
					                                    >
 | 
				
			||||||
                                    Delete
 | 
					                                        <Trash2 className="w-4 h-4" />
 | 
				
			||||||
                                </DropdownMenuItem>
 | 
					                                        Delete
 | 
				
			||||||
                            </DropdownMenuContent>
 | 
					                                    </DropdownMenuItem>
 | 
				
			||||||
                        </DropdownMenu>
 | 
					                                </DropdownMenuContent>
 | 
				
			||||||
                    </div>
 | 
					                            </DropdownMenu>
 | 
				
			||||||
                }
 | 
					                        </div>
 | 
				
			||||||
                <CardHeader>
 | 
					                    }
 | 
				
			||||||
                    <CardTitle>
 | 
					                </div>
 | 
				
			||||||
                        <StyledLink to={contentLocation}>
 | 
					 | 
				
			||||||
                            {data.title}
 | 
					 | 
				
			||||||
                        </StyledLink>
 | 
					 | 
				
			||||||
                    </CardTitle>
 | 
					 | 
				
			||||||
                    <CardDescription>
 | 
					 | 
				
			||||||
                        <StyledLink to={`/search?allow_tag=type:${classifiedTags.type[0] ?? ""}`} className="text-sm">
 | 
					 | 
				
			||||||
                            {classifiedTags.type[0] ?? "N/A"}
 | 
					 | 
				
			||||||
                        </StyledLink>
 | 
					 | 
				
			||||||
                    </CardDescription>
 | 
					 | 
				
			||||||
                </CardHeader>
 | 
					 | 
				
			||||||
                <CardContent>
 | 
					                <CardContent>
 | 
				
			||||||
                    <div className="grid gap-4 grid-cols-[repeat(auto_fill_300px)]">
 | 
					                    <div className="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
 | 
				
			||||||
                        <DescTagItem name="Artist" items={classifiedTags.artist} />
 | 
					                        <DescTagItem name="Artist" items={classifiedTags.artist}
 | 
				
			||||||
                        <DescTagItem name="Group" items={classifiedTags.group} />
 | 
					                            icon={<Paintbrush className="w-4 h-4" />} />
 | 
				
			||||||
                        <DescTagItem name="Series" items={classifiedTags.series} />
 | 
					
 | 
				
			||||||
                        <DescTagItem name="Character" items={classifiedTags.character} />
 | 
					                        <DescTagItem name="Group" items={classifiedTags.group}
 | 
				
			||||||
                        <DescItem name="Created At / Modified At">{new Date(data.created_at).toLocaleString()} / {new Date(data.modified_at).toLocaleString()}</DescItem>
 | 
					                            icon={<Users className="w-4 h-4" />} />
 | 
				
			||||||
                        <DescItem name="Filehash">{data.content_hash}</DescItem>
 | 
					
 | 
				
			||||||
                        <DescItem name="Path">{`${data.basepath}/${data.filename}`}</DescItem>
 | 
					                        <DescTagItem name="Series" items={classifiedTags.series}
 | 
				
			||||||
                        <DescItem name="Page Count">{data.pagenum}</DescItem>
 | 
					                            icon={<BookOpen className="w-4 h-4" />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <DescTagItem name="Character" items={classifiedTags.character}
 | 
				
			||||||
 | 
					                            icon={<User className="w-4 h-4" />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <DescItem name="Created At / Modified At"
 | 
				
			||||||
 | 
					                            icon={<Clock className="w-4 h-4" />}
 | 
				
			||||||
 | 
					                            className="md:col-span-2">
 | 
				
			||||||
 | 
					                            <div className="flex flex-wrap gap-x-2">
 | 
				
			||||||
 | 
					                                <span className="bg-muted/50 px-1.5 py-0.5 rounded text-sm">{new Date(data.created_at).toLocaleString()}</span> /
 | 
				
			||||||
 | 
					                                <span className="bg-muted/50 px-1.5 py-0.5 rounded text-sm">{new Date(data.modified_at).toLocaleString()}</span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </DescItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <DescItem name="Filehash"
 | 
				
			||||||
 | 
					                            icon={<FileCheck className="w-4 h-4" />}>
 | 
				
			||||||
 | 
					                            <span className="font-mono text-sm bg-muted/50 px-1.5 py-0.5 rounded overflow-x-auto block whitespace-nowrap max-w-full">{data.content_hash}</span>
 | 
				
			||||||
 | 
					                        </DescItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <DescItem name="Page Count"
 | 
				
			||||||
 | 
					                            icon={<Layers className="w-4 h-4" />}>
 | 
				
			||||||
 | 
					                            <span className="text-sm bg-primary/20 px-2 py-0.5 rounded-full font-medium">{data.pagenum} 페이지</span>
 | 
				
			||||||
 | 
					                        </DescItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <DescItem name="Path"
 | 
				
			||||||
 | 
					                            className="md:col-span-2"
 | 
				
			||||||
 | 
					                            icon={<FileText className="w-4 h-4" />}>
 | 
				
			||||||
 | 
					                            <span className="font-mono text-sm bg-muted/50 px-1.5 py-0.5 rounded overflow-x-auto text-wrap block whitespace-nowrap max-w-full">{`${data.basepath}/${data.filename}`}</span>
 | 
				
			||||||
 | 
					                        </DescItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div className="grid mt-4">
 | 
					                    <div className="grid mt-4">
 | 
				
			||||||
                        <span className="text-muted-foreground text-sm">Tags</span>
 | 
					                        <span className="text-muted-foreground text-sm">Tags</span>
 | 
				
			||||||
| 
						 | 
					@ -161,20 +217,37 @@ function SimilarContentCard({
 | 
				
			||||||
    const { data, error, isLoading } = useGalleryDocSimilar(id);
 | 
					    const { data, error, isLoading } = useGalleryDocSimilar(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isLoading) {
 | 
					    if (isLoading) {
 | 
				
			||||||
        return <div className="p-4">Loading...</div>
 | 
					        return <div className="p-4 mt-6 text-center animate-pulse bg-muted/20 rounded-lg">
 | 
				
			||||||
 | 
					            <Loader2 className="w-6 h-6 mx-auto mb-2 animate-spin opacity-50" />
 | 
				
			||||||
 | 
					            <span className="text-muted-foreground">유사 콘텐츠 불러오는 중...</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (error) {
 | 
					    if (error) {
 | 
				
			||||||
        return <div className="p-4">Error: {String(error)}</div>
 | 
					        return <div className="p-4 mt-6 text-destructive/70 bg-destructive/10 border border-destructive/20 rounded-lg">
 | 
				
			||||||
 | 
					            <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                <AlertCircle className="w-4 h-4" />
 | 
				
			||||||
 | 
					                오류: {String(error)}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!data) {
 | 
					    if (!data || data.length === 0) {
 | 
				
			||||||
        return <div className="p-4">Not found</div>
 | 
					        return <div className="p-6 mt-6 text-center bg-muted/10 rounded-lg border border-border/50">
 | 
				
			||||||
 | 
					            <FileQuestion className="w-9 h-9 mx-auto mb-3 opacity-30" />
 | 
				
			||||||
 | 
					            <span className="text-muted-foreground">유사한 콘텐츠가 없습니다.</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <div className="space-y-4 mt-4 mx-2">
 | 
					        <div className="space-y-4 mt-4 mx-2">
 | 
				
			||||||
            <h2 className="text-2xl font-bold">Contents with Similar Tags</h2>
 | 
					            <div className="flex items-center mb-4 gap-2">
 | 
				
			||||||
 | 
					                <h2 className="text-2xl font-bold bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">유사한 콘텐츠</h2>
 | 
				
			||||||
 | 
					                <div className="h-[2px] flex-1 bg-gradient-to-r from-border to-transparent"></div>
 | 
				
			||||||
 | 
					                <span className="text-sm text-muted-foreground bg-secondary/20 px-2 py-0.5 rounded-full">
 | 
				
			||||||
 | 
					                    {data.length}개 항목
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
            <div className="grid gap-4 grid-cols-[repeat(auto_fill_300px)]">
 | 
					            <div className="grid gap-4 grid-cols-[repeat(auto_fill_300px)]">
 | 
				
			||||||
                {data.map((doc) => (
 | 
					                {data.map((doc) => (
 | 
				
			||||||
                    <GalleryCard key={doc.id} doc={doc} />
 | 
					                    <GalleryCard key={doc.id} doc={doc} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue