diff --git a/next.config.ts b/next.config.ts
index 810a8dd..9d5b0f1 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -8,6 +8,14 @@ const nextConfig: NextConfig = {
fullUrl: true,
},
},
+ images: {
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: 'images.unsplash.com',
+ },
+ ],
+ },
};
export default nextConfig;
diff --git a/src/components/ui/Avatar/Avatar.tsx b/src/components/ui/Avatar/Avatar.tsx
new file mode 100644
index 0000000..b7eea99
--- /dev/null
+++ b/src/components/ui/Avatar/Avatar.tsx
@@ -0,0 +1,70 @@
+'use client';
+
+import { DEFAULT_COLORS } from '@/constants/colors';
+import { cn, getColorByString } from '@/utils/helper';
+import { cva, VariantProps } from 'class-variance-authority';
+import Image from 'next/image';
+import { HTMLAttributes, useState } from 'react';
+
+/**
+ * 커스터마이징 가능한 기본 공용 Avatar 컴포넌트입니다.
+ *
+ * ### Variants:
+ * - `size`:
+ * - `default`: width:38px, font-size:16px;
+ * - `md`: width:34px, font-size:16px;
+ * - `sm`: width:24px, font-size:12px;
+ *
+ * ### Default Variants (기본 스타일):
+ * - `size`: `default`
+ *
+ *
+ * @example
+ *
+ *
+ * 반응형으로 쓰고 싶을땐, classname을 추가하세요
+ *
+ */
+
+const avatarVariants = cva(
+ //prettier-ignore
+ 'relative inline-flex items-center justify-center aspect-square rounded-full overflow-hidden border-2 border-white leading-none',
+ {
+ variants: {
+ size: {
+ default: 'w-[38px] text-lg',
+ md: 'w-[34px] text-lg',
+ sm: 'w-6 text-xs',
+ },
+ },
+
+ defaultVariants: {
+ size: 'default',
+ },
+ },
+);
+
+interface AvatarProps extends HTMLAttributes, VariantProps {
+ email: string;
+ profileImageUrl?: string;
+}
+
+export default function Avatar({ email, profileImageUrl, size, className, ...props }: AvatarProps) {
+ const [imgError, setImgError] = useState(false);
+ const colorCode = getColorByString(email, DEFAULT_COLORS);
+ const firstChar = email.charAt(0);
+
+ const isFallback = !profileImageUrl || imgError;
+
+ return (
+
+ {!isFallback ? (
+ setImgError(true)} />
+ ) : (
+
+ {firstChar}
+
+ )}
+
+ );
+}
diff --git a/src/components/ui/Avatar/StackAvatars.tsx b/src/components/ui/Avatar/StackAvatars.tsx
new file mode 100644
index 0000000..df31494
--- /dev/null
+++ b/src/components/ui/Avatar/StackAvatars.tsx
@@ -0,0 +1,26 @@
+import Avatar from './Avatar';
+
+interface StackAvatarsProps {
+ members: { email: string; profileImageUrl?: string }[];
+ visibleCount: number;
+}
+
+export default function StackAvatars({ members, visibleCount = 3 }: StackAvatarsProps) {
+ const chunkedMembers = members.slice(0, visibleCount);
+ const restMembersCount = members.length - visibleCount;
+
+ return (
+
+ {chunkedMembers.map((member) => (
+ -
+
+
+ ))}
+ {restMembersCount > 0 && (
+ -
+ +{restMembersCount}
+
+ )}
+
+ );
+}
diff --git a/src/constants/colors.ts b/src/constants/colors.ts
new file mode 100644
index 0000000..7abcc32
--- /dev/null
+++ b/src/constants/colors.ts
@@ -0,0 +1,3 @@
+type HexColor = `#${string}`;
+
+export const DEFAULT_COLORS: HexColor[] = ['#7AC555', '#760DDE', '#FFA500', '#76A5EA', '#E876EA'];
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index 2819a83..b4de34b 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -4,3 +4,9 @@ import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+export function getColorByString(value: string, colorArray: string[]) {
+ const charCode = value.toLowerCase().charCodeAt(0);
+ const index = charCode % colorArray.length;
+ return colorArray[index];
+}