-
-
- {!rssConsent && (
-
- RSS 수집을 비활성화하면 글을 직접 등록해야 합니다.
-
- )}
+
+ {blogs.map((blog, idx) => (
+
+
+
블로그 {idx + 1}
+ {blogs.length > 1 && (
+
+ )}
+
+
+
+ updateBlog(blog.key, { label: e.target.value })}
+ maxLength={100}
+ />
+
+
+
+ updateBlog(blog.key, { blogUrl: e.target.value })}
+ maxLength={500}
+ />
+
+
+
+
+ {!blog.rssConsent && (
+
+ 끄면 이 블로그 글을 직접 등록해야 합니다.
+
+ )}
+
+
-
-
+ ))}
+ {blogs.length < MAX_BLOGS && (
+
+ )}
@@ -511,7 +603,7 @@ export default function ProfileEditPage() {
saving ||
!name.trim() ||
!nickname.trim() ||
- !blogUrl.trim() ||
+ !blogsValid ||
interests.length < 3 ||
bio.trim().length < 100
}
diff --git a/packages/web/src/app/(user)/profile/onboarding/page.tsx b/packages/web/src/app/(user)/profile/onboarding/page.tsx
index 2fe4b27..9d2713c 100644
--- a/packages/web/src/app/(user)/profile/onboarding/page.tsx
+++ b/packages/web/src/app/(user)/profile/onboarding/page.tsx
@@ -2,7 +2,17 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
-import { ArrowLeft, ArrowRight, Check, Heart, Loader2, Target, User } from 'lucide-react';
+import {
+ ArrowLeft,
+ ArrowRight,
+ Check,
+ Heart,
+ Loader2,
+ Plus,
+ Target,
+ Trash2,
+ User,
+} from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -18,6 +28,19 @@ import { FormPageSkeleton } from '@/components/ui/page-state';
const TOTAL_STEPS = 3;
+// 멤버당 블로그 최대 개수 (서버 MAX_BLOGS_PER_MEMBER와 동일, 서버가 최종 검증)
+const MAX_BLOGS = 3;
+
+interface BlogItem {
+ key: string;
+ label: string;
+ blogUrl: string;
+ rssConsent: boolean;
+}
+
+let blogKeySeq = 0;
+const newBlogKey = () => `blog-${Date.now()}-${blogKeySeq++}`;
+
export default function OnboardingPage() {
const router = useRouter();
const [loading, setLoading] = useState(true);
@@ -30,7 +53,9 @@ export default function OnboardingPage() {
const [nickname, setNickname] = useState('');
const [selectedPart, setSelectedPart] = useState('');
const [customPart, setCustomPart] = useState('');
- const [blogUrl, setBlogUrl] = useState('');
+ const [blogs, setBlogs] = useState
([
+ { key: newBlogKey(), label: '', blogUrl: '', rssConsent: true },
+ ]);
const [interests, setInterests] = useState([]);
const [bio, setBio] = useState('');
const [profileImageUrl, setProfileImageUrl] = useState('');
@@ -39,7 +64,6 @@ export default function OnboardingPage() {
const [githubUrl, setGithubUrl] = useState('');
const [linkedinUrl, setLinkedinUrl] = useState('');
const [instagramUrl, setInstagramUrl] = useState('');
- const [rssConsent, setRssConsent] = useState(true);
useEffect(() => {
const fetchUserInfo = async () => {
@@ -82,12 +106,29 @@ export default function OnboardingPage() {
});
};
+ const addBlog = () => {
+ setBlogs((prev) =>
+ prev.length >= MAX_BLOGS
+ ? prev
+ : [...prev, { key: newBlogKey(), label: '', blogUrl: '', rssConsent: true }]
+ );
+ };
+
+ const removeBlog = (key: string) => {
+ setBlogs((prev) => (prev.length <= 1 ? prev : prev.filter((b) => b.key !== key)));
+ };
+
+ const updateBlog = (key: string, patch: Partial>) => {
+ setBlogs((prev) => prev.map((b) => (b.key === key ? { ...b, ...patch } : b)));
+ };
+
const part = selectedPart === 'other' ? customPart.trim() : selectedPart;
+ const blogsValid =
+ blogs.length >= 1 &&
+ blogs.length <= MAX_BLOGS &&
+ blogs.every((b) => b.blogUrl.trim().length > 0);
const isStep1Valid =
- name.trim().length > 0 &&
- nickname.trim().length > 0 &&
- part.length > 0 &&
- blogUrl.trim().length > 0;
+ name.trim().length > 0 && nickname.trim().length > 0 && part.length > 0 && blogsValid;
const isStep2Valid = interests.length >= 3 && interests.length <= 6 && bio.trim().length >= 100;
const isStep3Valid = resolution.trim().length > 0;
@@ -111,7 +152,7 @@ export default function OnboardingPage() {
if (!name.trim()) missing.push('이름');
if (!nickname.trim()) missing.push('닉네임');
if (!part) missing.push('파트');
- if (!blogUrl.trim()) missing.push('블로그 URL');
+ if (!blogsValid) missing.push('블로그 URL');
if (missing.length > 0) toast.error(`${missing.join(', ')}을(를) 입력해주세요.`);
break;
}
@@ -140,7 +181,11 @@ export default function OnboardingPage() {
name: name.trim(),
nickname: nickname.trim(),
part,
- blogUrl: blogUrl.trim(),
+ blogs: blogs.map((b) => ({
+ label: b.label.trim() || null,
+ blogUrl: b.blogUrl.trim(),
+ rssConsent: b.rssConsent,
+ })),
profileImageUrl: profileImageUrl || null,
bio: bio.trim(),
interests,
@@ -148,7 +193,6 @@ export default function OnboardingPage() {
githubUrl: githubUrl.trim() || null,
linkedinUrl: linkedinUrl.trim() || null,
instagramUrl: instagramUrl.trim() || null,
- rssConsent,
}),
});
@@ -274,36 +318,70 @@ export default function OnboardingPage() {
)}
-