From a7a751c79166136eceba10bedf1a4ec92e39ca99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8B=A0=EC=9A=B1?= Date: Tue, 17 Feb 2026 00:50:13 +0900 Subject: [PATCH 01/36] refactor: refactor 1 --- src/app/(public)/signup/[step]/page.tsx | 38 ++++++ src/app/(public)/signup/layout.tsx | 15 ++- src/app/(public)/signup/page.tsx | 37 ++---- src/components/base-ui/Join/JoinButton.tsx | 7 +- src/components/base-ui/Join/JoinInput.tsx | 27 ++-- src/components/base-ui/Join/JoinLayout.tsx | 31 ++++- .../EmailVerification/EmailVerification.tsx | 31 +---- .../steps/PasswordEntry/PasswordEntry.tsx | 7 +- .../Join/steps/ProfileImage/ProfileImage.tsx | 43 ++++-- .../steps/ProfileImage/useProfileImage.ts | 27 ++-- .../Join/steps/ProfileSetup/ProfileSetup.tsx | 71 +++++----- .../steps/ProfileSetup/useProfileSetup.ts | 36 +++-- .../steps/SignupComplete/useSignupComplete.ts | 13 +- .../steps/TermsAgreement/TermsAgreement.tsx | 12 +- .../Join/steps/useEmailVerification.ts | 60 +++------ src/contexts/SignupContext.tsx | 123 ++++++++++++++++++ 16 files changed, 377 insertions(+), 201 deletions(-) create mode 100644 src/app/(public)/signup/[step]/page.tsx create mode 100644 src/contexts/SignupContext.tsx diff --git a/src/app/(public)/signup/[step]/page.tsx b/src/app/(public)/signup/[step]/page.tsx new file mode 100644 index 0000000..ccd72d3 --- /dev/null +++ b/src/app/(public)/signup/[step]/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import React from "react"; +import { useParams, useRouter } from "next/navigation"; +import TermsAgreement from "@/components/base-ui/Join/steps/TermsAgreement/TermsAgreement"; +import EmailVerification from "@/components/base-ui/Join/steps/EmailVerification/EmailVerification"; +import PasswordEntry from "@/components/base-ui/Join/steps/PasswordEntry/PasswordEntry"; +import ProfileSetup from "@/components/base-ui/Join/steps/ProfileSetup/ProfileSetup"; +import ProfileImage from "@/components/base-ui/Join/steps/ProfileImage/ProfileImage"; +import SignupComplete from "@/components/base-ui/Join/steps/SignupComplete/SignupComplete"; + +export default function SignupStepPage() { + const params = useParams(); + const router = useRouter(); + const step = params.step as string; + + const navigateTo = (nextStep: string) => { + router.push(`/signup/${nextStep}`); + }; + + const steps: Record = { + terms: navigateTo("email")} />, + email: navigateTo("password")} />, + password: navigateTo("profile")} />, + profile: navigateTo("profile-image")} />, + "profile-image": navigateTo("complete")} />, + complete: , + }; + + const currentStep = steps[step]; + + if (!currentStep) { + // Falls back to terms if step is invalid + return null; + } + + return <>{currentStep}; +} diff --git a/src/app/(public)/signup/layout.tsx b/src/app/(public)/signup/layout.tsx index 8b21600..835a483 100644 --- a/src/app/(public)/signup/layout.tsx +++ b/src/app/(public)/signup/layout.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { SignupProvider } from "@/contexts/SignupContext"; export default function SignupLayout({ children, @@ -6,11 +7,13 @@ export default function SignupLayout({ children: React.ReactNode; }) { return ( -
- {children} -
+ +
+ {children} +
+
); } \ No newline at end of file diff --git a/src/app/(public)/signup/page.tsx b/src/app/(public)/signup/page.tsx index a35a982..b916629 100644 --- a/src/app/(public)/signup/page.tsx +++ b/src/app/(public)/signup/page.tsx @@ -1,35 +1,14 @@ "use client"; -import React, { useState } from "react"; -import TermsAgreement from "@/components/base-ui/Join/steps/TermsAgreement/TermsAgreement"; -import EmailVerification from "@/components/base-ui/Join/steps/EmailVerification/EmailVerification"; -import PasswordEntry from "@/components/base-ui/Join/steps/PasswordEntry/PasswordEntry"; -import ProfileSetup from "@/components/base-ui/Join/steps/ProfileSetup/ProfileSetup"; -import ProfileImage from "@/components/base-ui/Join/steps/ProfileImage/ProfileImage"; -import SignupComplete from "@/components/base-ui/Join/steps/SignupComplete/SignupComplete"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; export default function SignupPage() { - const [step, setStep] = useState< - "terms" | "email" | "password" | "profile" | "profile-image" | "complete" - >("terms"); - const steps = { - terms: ( - { - setStep("email"); - }} - /> - ), - email: setStep("password")} />, - password: setStep("profile")} />, - profile: setStep("profile-image")} />, - "profile-image": setStep("complete")} />, - complete: , - }; + const router = useRouter(); - return ( - <> - {steps[step]} - - ); + useEffect(() => { + router.replace("/signup/terms"); + }, [router]); + + return null; } diff --git a/src/components/base-ui/Join/JoinButton.tsx b/src/components/base-ui/Join/JoinButton.tsx index a0f9be8..1724b4a 100644 --- a/src/components/base-ui/Join/JoinButton.tsx +++ b/src/components/base-ui/Join/JoinButton.tsx @@ -18,7 +18,7 @@ const JoinButton: React.FC = ({ const variants = { primary: disabled ? "bg-[#DADADA] text-[#8D8D8D] cursor-not-allowed" - : "bg-[#7B6154] text-[#FFF]", + : "bg-[#7B6154] text-[#FFF] hover:bg-[#7B6154] hover:text-[#FFF]", secondary: "bg-[#EAE5E2] text-[#5E4A40] border border-[#D2C5B6]", }; @@ -26,9 +26,8 @@ const JoinButton: React.FC = ({ const widthClass = className?.includes("w-") ? "" : "w-[526px]"; return ( @@ -77,18 +74,17 @@ const EmailVerification: React.FC = ({ onNext }) => { - {/* Next Button: CTA_2 */} + {/* Next Button */} = ({ onNext }) => { > 다음 - - {/* Toast Notification */} - {showToast && ( -
- - 인증이 완료되었습니다. - -
- )} ); diff --git a/src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx b/src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx index ec293e3..9f3d0ad 100644 --- a/src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx +++ b/src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx @@ -1,17 +1,17 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import JoinLayout from "@/components/base-ui/Join/JoinLayout"; import JoinButton from "@/components/base-ui/Join/JoinButton"; import JoinInput from "@/components/base-ui/Join/JoinInput"; +import { useSignup } from "@/contexts/SignupContext"; interface PasswordEntryProps { onNext: () => void; } const PasswordEntry: React.FC = ({ onNext }) => { - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); + const { password, setPassword, confirmPassword, setConfirmPassword } = useSignup(); // 유효성 검사: 비밀번호가 입력되었고, 두 비밀번호가 일치하는지 확인 // 스펙상 "20자 이내" 제한 @@ -26,6 +26,7 @@ const PasswordEntry: React.FC = ({ onNext }) => { {/* Password Input */} void; @@ -15,14 +16,17 @@ const ProfileImage: React.FC = ({ onNext }) => { const { selectedInterests, profileImage, + isProfileImageSet, toggleInterest, handleResetImage, handleImageUpload, isValid, } = useProfileImage(); + const { showToast } = useSignup(); const [mobileStep, setMobileStep] = useState<"image" | "interest">("image"); const [isMobile, setIsMobile] = useState(false); + const interestRef = React.useRef(null); useEffect(() => { const checkMobile = () => { @@ -37,20 +41,34 @@ const ProfileImage: React.FC = ({ onNext }) => { }, []); const handleNextClick = () => { + // 1. 프로필 사진 업로드 안하거나, 기본 프로필 사진 사용 클릭 안하고 다음 버튼 클릭시 + if (!isProfileImageSet && (isMobile ? mobileStep === "image" : true)) { + showToast("프로필 사진을 입력해주세요!"); + return; + } + if (isMobile && mobileStep === "image") { setMobileStep("interest"); - } else { + return; + } + + // 2. 관심 독서 카테고리를 선택하지 않고 다음 버튼 클릭시 + if (selectedInterests.length === 0) { + showToast("관심 독서 카테고리를 최소 1개 선택해주세요!"); + interestRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + return; + } + + if (isValid) { onNext?.(); } }; // Logic for disabling the button - // Desktop: !isValid (must select at least 1 interest) - // Mobile Step 1 (Image): Always enabled (image is optional or default provided) - // Mobile Step 2 (Interest): !isValid - const isButtonDisabled = isMobile - ? mobileStep === "interest" && !isValid - : !isValid; + // 버튼은 항상 활성화 해두고 클릭 시 토스트로 피드백 (기능 명세에 '필수 입력 필드 채워졌을 때 활성화'라고 되어있으나 + // 토스트 피드백을 보여주기 위해 클릭 가능하게 유지하는 것이 일반적인 UI 패턴임. + // 만약 비활성화가 우선이라면 토스트를 보여줄 수 없음.) + const isButtonDisabled = false; // Dynamic Header Title const headerTitle = isMobile @@ -66,9 +84,8 @@ const ProfileImage: React.FC = ({ onNext }) => {
{/* Profile Image Uploader: Visible on Desktop OR Mobile Step 1 */}
= ({ onNext }) => { {/* Interest Selector: Visible on Desktop OR Mobile Step 2 */}
{ - const [selectedInterests, setSelectedInterests] = useState([]); - const [profileImage, setProfileImage] = useState( - "/default_profile_1.svg" - ); + const { + selectedInterests, + setSelectedInterests, + profileImage, + setProfileImage, + isProfileImageSet, + setIsProfileImageSet, + showToast, + } = useSignup(); const toggleInterest = (category: string) => { if (selectedInterests.includes(category)) { - setSelectedInterests((prev) => prev.filter((c) => c !== category)); + setSelectedInterests(selectedInterests.filter((c) => c !== category)); } else { if (selectedInterests.length < 6) { - setSelectedInterests((prev) => [...prev, category]); + setSelectedInterests([...selectedInterests, category]); + } else { + showToast("카테고리는 최대 6개까지 선택 가능합니다."); } } }; const handleResetImage = () => { setProfileImage("/default_profile_1.svg"); + setIsProfileImageSet(true); }; const handleImageUpload = (e: React.ChangeEvent) => { @@ -43,6 +52,7 @@ export const useProfileImage = () => { if (file) { const imageUrl = URL.createObjectURL(file); setProfileImage(imageUrl); + setIsProfileImageSet(true); } }; @@ -55,11 +65,12 @@ export const useProfileImage = () => { }; }, [profileImage]); - const isValid = selectedInterests.length >= 1; + const isValid = selectedInterests.length >= 1 && selectedInterests.length <= 6; return { selectedInterests, profileImage, + isProfileImageSet, toggleInterest, handleResetImage, handleImageUpload, diff --git a/src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx b/src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx index 607495e..e4f0c58 100644 --- a/src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx +++ b/src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx @@ -3,6 +3,7 @@ import JoinLayout from "../../JoinLayout"; import JoinButton from "../../JoinButton"; import JoinInput from "../../JoinInput"; import { useProfileSetup } from "./useProfileSetup"; +import { useSignup } from "@/contexts/SignupContext"; interface ProfileSetupProps { onNext?: () => void; @@ -12,6 +13,7 @@ const ProfileSetup: React.FC = ({ onNext }) => { const { nickname, isNicknameChecked, + isNicknameValid, intro, name, phone, @@ -22,6 +24,19 @@ const ProfileSetup: React.FC = ({ onNext }) => { handleCheckDuplicate, isValid, } = useProfileSetup(); + const { showToast } = useSignup(); + const nicknameRef = React.useRef(null); + + const handleNextClick = () => { + if (!isNicknameChecked) { + showToast("닉네임 중복확인을 해주세요!"); + nicknameRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + return; + } + if (isValid) { + onNext?.(); + } + }; return ( @@ -31,7 +46,7 @@ const ProfileSetup: React.FC = ({ onNext }) => { {/* Group 1: Nickname & Intro */}
{/* 닉네임 섹션 */} -
+
닉네임 @@ -42,7 +57,6 @@ const ProfileSetup: React.FC = ({ onNext }) => { @@ -52,7 +66,6 @@ const ProfileSetup: React.FC = ({ onNext }) => { @@ -60,17 +73,16 @@ const ProfileSetup: React.FC = ({ onNext }) => {
- 중복확인 + {isNicknameChecked ? "확인됨" : "중복확인"}
-
- {/* 한줄소개 */} + {/* 한줄소개 */}
소개 @@ -84,17 +96,17 @@ const ProfileSetup: React.FC = ({ onNext }) => {
- {/* Group 2: Name & Phone */} + {/* Group 2: Name & Phone */}
{/* 이름 */}
이름 -
@@ -105,24 +117,23 @@ const ProfileSetup: React.FC = ({ onNext }) => { 전화번호 +
+
- value={phone} - onChange={handlePhoneChange} - placeholder="010-0000-0000" - className="h-[36px] t:h-[44px] py-0 t:py-[12px] border-Subbrown-4 placeholder-Gray-3 text-[14px] font-normal bg-white" - /> + + 다음 +
- - - 다음 - - - ); }; diff --git a/src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts b/src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts index b62d676..750f0bd 100644 --- a/src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts +++ b/src/components/base-ui/Join/steps/ProfileSetup/useProfileSetup.ts @@ -1,24 +1,34 @@ -import { useState } from "react"; +import { useSignup } from "@/contexts/SignupContext"; export const useProfileSetup = () => { - const [nickname, setNickname] = useState(""); - const [isNicknameChecked, setIsNicknameChecked] = useState(false); - - const [intro, setIntro] = useState(""); - const [name, setName] = useState(""); - const [phone, setPhone] = useState(""); + const { + nickname, + setNickname, + isNicknameChecked, + setIsNicknameChecked, + intro, + setIntro, + name, + setName, + phone, + setPhone, + showToast, + } = useSignup(); const handleNicknameChange = (e: React.ChangeEvent) => { - setNickname(e.target.value); + const value = e.target.value; + // 닉네임 : 영어 소문자 및 특수문자, 숫자만 사용 가능, 최대 20글자 + const filteredValue = value.replace(/[^a-z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/g, "").slice(0, 20); + setNickname(filteredValue); setIsNicknameChecked(false); }; const handleIntroChange = (e: React.ChangeEvent) => { - setIntro(e.target.value); + setIntro(e.target.value.slice(0, 40)); // 최대 40자 }; const handleNameChange = (e: React.ChangeEvent) => { - setName(e.target.value); + setName(e.target.value.slice(0, 10)); // 10자 제한 }; const handlePhoneChange = (e: React.ChangeEvent) => { @@ -42,10 +52,15 @@ export const useProfileSetup = () => { const handleCheckDuplicate = () => { if (!nickname) return; + + // 더미 로직: 중복 확인 결과 표시 console.log("Check duplicate nickname:", nickname); setIsNicknameChecked(true); + showToast("사용 가능한 닉네임입니다."); }; + const isNicknameValid = nickname.length > 0; + const isValid = nickname !== "" && isNicknameChecked && @@ -56,6 +71,7 @@ export const useProfileSetup = () => { return { nickname, isNicknameChecked, + isNicknameValid, intro, name, phone, diff --git a/src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts b/src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts index 25aca3f..5cade25 100644 --- a/src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts +++ b/src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts @@ -1,12 +1,9 @@ import { useRouter } from "next/navigation"; +import { useSignup } from "@/contexts/SignupContext"; export const useSignupComplete = () => { const router = useRouter(); - - // 더미 데이터 - const nickname = "닉네임"; - const intro = "안녕하세요~ 윤현일입니다."; - const profileImage = "/profile.svg"; // 이미지가 없을 경우 /default_profile_1.svg 사용 권장 + const { nickname, intro, profileImage } = useSignup(); const handleSearchMeeting = () => { console.log("Search Meeting clicked"); @@ -24,9 +21,9 @@ export const useSignupComplete = () => { }; return { - nickname, - intro, - profileImage, + nickname: nickname || "닉네임", + intro: intro || "안녕하세요~", + profileImage: profileImage || "/profile.svg", handleSearchMeeting, handleCreateMeeting, handleUseWithoutMeeting, diff --git a/src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx b/src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx index 95abca8..5896075 100644 --- a/src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx +++ b/src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx @@ -2,10 +2,11 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import Image from "next/image"; import JoinLayout from "@/components/base-ui/Join/JoinLayout"; import JoinButton from "@/components/base-ui/Join/JoinButton"; +import { useSignup } from "@/contexts/SignupContext"; export const TERMS_DATA = [ { @@ -27,12 +28,7 @@ interface TermsAgreementProps { } const TermsAgreement: React.FC = ({ onNext }) => { - const initialAgreements = TERMS_DATA.reduce((acc, term) => { - acc[term.id] = false; - return acc; - }, {} as Record); - - const [agreements, setAgreements] = useState(initialAgreements); + const { agreements, setAgreements } = useSignup(); const allAgreed = TERMS_DATA.every((term) => agreements[term.id]); const isButtonEnabled = TERMS_DATA.filter((term) => term.required).every( @@ -44,7 +40,7 @@ const TermsAgreement: React.FC = ({ onNext }) => { }; const handleAgreementChange = (id: string, checked: boolean) => { - setAgreements((prev) => ({ ...prev, [id]: checked })); + setAgreements({ ...agreements, [id]: checked }); }; const handleAllAgreementChange = () => { diff --git a/src/components/base-ui/Join/steps/useEmailVerification.ts b/src/components/base-ui/Join/steps/useEmailVerification.ts index 57cbe93..78dfa5f 100644 --- a/src/components/base-ui/Join/steps/useEmailVerification.ts +++ b/src/components/base-ui/Join/steps/useEmailVerification.ts @@ -1,24 +1,24 @@ -// c:\Users\shinwookKang\Desktop\CheckMo\FE\src\components\base-ui\Join\steps\useEmailVerification.tsx - -import { useState, useEffect } from "react"; +import { useEffect } from "react"; +import { useSignup } from "@/contexts/SignupContext"; export const useEmailVerification = () => { - const [email, setEmail] = useState(""); - const [isEmailValid, setIsEmailValid] = useState(false); - const [timeLeft, setTimeLeft] = useState(null); - const [verificationCode, setVerificationCode] = useState(""); - const [isCodeValid, setIsCodeValid] = useState(false); - const [isVerified, setIsVerified] = useState(false); + const { + email, + setEmail, + verificationCode, + setVerificationCode, + isVerified, + setIsVerified, + timeLeft, + setTimeLeft, + showToast, + } = useSignup(); - // Toast Animation State - const [showToast, setShowToast] = useState(false); // Controls mounting - const [isToastVisible, setIsToastVisible] = useState(false); // Controls opacity + const isEmailValid = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); + const isCodeValid = verificationCode.length === 6; const handleEmailChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setEmail(value); - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - setIsEmailValid(emailRegex.test(value)); + setEmail(e.target.value); }; const handleVerificationCodeChange = ( @@ -27,7 +27,6 @@ export const useEmailVerification = () => { const value = e.target.value; if (/^\d*$/.test(value) && value.length <= 6) { setVerificationCode(value); - setIsCodeValid(value.length === 6); } }; @@ -40,7 +39,7 @@ export const useEmailVerification = () => { const handleVerify = () => { if (isCodeValid) { setIsVerified(true); - setShowToast(true); + showToast("인증이 완료되었습니다."); } }; @@ -53,29 +52,10 @@ export const useEmailVerification = () => { useEffect(() => { if (timeLeft === null || timeLeft === 0) return; const interval = setInterval(() => { - setTimeLeft((prev) => (prev !== null && prev > 0 ? prev - 1 : 0)); + setTimeLeft(timeLeft > 0 ? timeLeft - 1 : 0); }, 1000); return () => clearInterval(interval); - }, [timeLeft]); - - useEffect(() => { - if (showToast) { - // 1. Mount (showToast=true) -> Wait 10ms -> Fade In (isToastVisible=true) - const showTimer = setTimeout(() => setIsToastVisible(true), 10); - - // 2. Wait 3000ms -> Fade Out (isToastVisible=false) - const hideTimer = setTimeout(() => setIsToastVisible(false), 3000); - - // 3. Wait 3300ms (allow transition) -> Unmount (showToast=false) - const unmountTimer = setTimeout(() => setShowToast(false), 3300); - - return () => { - clearTimeout(showTimer); - clearTimeout(hideTimer); - clearTimeout(unmountTimer); - }; - } - }, [showToast]); + }, [timeLeft, setTimeLeft]); return { email, @@ -88,8 +68,6 @@ export const useEmailVerification = () => { startTimer, isVerified, handleVerify, - showToast, - isToastVisible, // Now correctly exported formatTime, }; }; diff --git a/src/contexts/SignupContext.tsx b/src/contexts/SignupContext.tsx new file mode 100644 index 0000000..2e85bab --- /dev/null +++ b/src/contexts/SignupContext.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React, { createContext, useContext, useState, ReactNode, useCallback } from "react"; + +interface SignupState { + // Terms + agreements: Record; + // Email + email: string; + verificationCode: string; + isVerified: boolean; + timeLeft: number | null; + // Password + password: string; + confirmPassword: string; + // Profile + nickname: string; + intro: string; + name: string; + phone: string; + isNicknameChecked: boolean; + // Profile Image & Interests + profileImage: string | null; + isProfileImageSet: boolean; // Explicit check for whether user set an image or chose default + selectedInterests: string[]; + // Toast + toast: { message: string; visible: boolean } | null; +} + +interface SignupActions { + setAgreements: (agreements: Record) => void; + setEmail: (email: string) => void; + setVerificationCode: (code: string) => void; + setIsVerified: (verified: boolean) => void; + setTimeLeft: (time: number | null) => void; + setPassword: (password: string) => void; + setConfirmPassword: (password: string) => void; + setNickname: (nickname: string) => void; + setIntro: (intro: string) => void; + setName: (name: string) => void; + setPhone: (phone: string) => void; + setIsNicknameChecked: (checked: boolean) => void; + setProfileImage: (image: string | null) => void; + setIsProfileImageSet: (isSet: boolean) => void; + setSelectedInterests: (interests: string[]) => void; + showToast: (message: string) => void; + resetSignup: () => void; +} + +interface SignupContextType extends SignupState, SignupActions { } + +const initialState: SignupState = { + agreements: {}, + email: "", + verificationCode: "", + isVerified: false, + timeLeft: null, + password: "", + confirmPassword: "", + nickname: "", + intro: "", + name: "", + phone: "", + isNicknameChecked: false, + profileImage: "/default_profile_1.svg", + isProfileImageSet: false, + selectedInterests: [], + toast: null, +}; + +const SignupContext = createContext(undefined); + +export const SignupProvider = ({ children }: { children: ReactNode }) => { + const [state, setState] = useState(initialState); + + const showToast = useCallback((message: string) => { + setState((prev) => ({ ...prev, toast: { message, visible: true } })); + + // Fade out after 2.5s + setTimeout(() => { + setState((prev) => prev.toast ? { ...prev, toast: { ...prev.toast, visible: false } } : prev); + }, 2500); + + // Unmount after 3s + setTimeout(() => { + setState((prev) => ({ ...prev, toast: null })); + }, 3000); + }, []); + + const actions: SignupActions = { + setAgreements: (agreements) => setState((prev) => ({ ...prev, agreements })), + setEmail: (email) => setState((prev) => ({ ...prev, email })), + setVerificationCode: (verificationCode) => setState((prev) => ({ ...prev, verificationCode })), + setIsVerified: (isVerified) => setState((prev) => ({ ...prev, isVerified })), + setTimeLeft: (timeLeft) => setState((prev) => ({ ...prev, timeLeft })), + setPassword: (password) => setState((prev) => ({ ...prev, password })), + setConfirmPassword: (confirmPassword) => setState((prev) => ({ ...prev, confirmPassword })), + setNickname: (nickname) => setState((prev) => ({ ...prev, nickname })), + setIntro: (intro) => setState((prev) => ({ ...prev, intro })), + setName: (name) => setState((prev) => ({ ...prev, name })), + setPhone: (phone) => setState((prev) => ({ ...prev, phone })), + setIsNicknameChecked: (isNicknameChecked) => setState((prev) => ({ ...prev, isNicknameChecked })), + setProfileImage: (profileImage) => setState((prev) => ({ ...prev, profileImage })), + setIsProfileImageSet: (isProfileImageSet) => setState((prev) => ({ ...prev, isProfileImageSet })), + setSelectedInterests: (selectedInterests) => setState((prev) => ({ ...prev, selectedInterests })), + showToast, + resetSignup: () => setState(initialState), + }; + + return ( + + {children} + + ); +}; + +export const useSignup = () => { + const context = useContext(SignupContext); + if (!context) { + throw new Error("useSignup must be used within a SignupProvider"); + } + return context; +}; From bab8330334a2c4f8e215791b29ca68d881b2543c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8B=A0=EC=9A=B1?= Date: Tue, 17 Feb 2026 01:14:17 +0900 Subject: [PATCH 02/36] =?UTF-8?q?refactor:=20=EC=95=BD=EA=B4=80=20?= =?UTF-8?q?=EB=8F=99=EC=9D=98=EC=84=9C=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=82=98=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=ED=99=94=EB=A9=B4=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=84=88=EB=B9=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/base-ui/Join/JoinButton.tsx | 19 +- src/components/base-ui/Join/JoinInput.tsx | 17 +- .../Join/steps/ProfileSetup/ProfileSetup.tsx | 2 +- .../steps/TermsAgreement/TermsAgreement.tsx | 249 +++++++++++++++++- 4 files changed, 274 insertions(+), 13 deletions(-) diff --git a/src/components/base-ui/Join/JoinButton.tsx b/src/components/base-ui/Join/JoinButton.tsx index 1724b4a..f90caa0 100644 --- a/src/components/base-ui/Join/JoinButton.tsx +++ b/src/components/base-ui/Join/JoinButton.tsx @@ -22,11 +22,24 @@ const JoinButton: React.FC = ({ secondary: "bg-[#EAE5E2] text-[#5E4A40] border border-[#D2C5B6]", }; - // className에 width 관련 클래스가 없으면 기본값 적용 - const widthClass = className?.includes("w-") ? "" : "w-[526px]"; + // Determine classes to avoid conflicts with className prop + const hasWidth = className?.includes("w-"); + const hasHeight = className?.includes("h-"); + const hasPx = + className?.includes("px-") || + className?.includes("pl-") || + className?.includes("pr-") || + className?.includes("p-"); + const hasPy = + className?.includes("py-") || + className?.includes("pt-") || + className?.includes("pb-") || + className?.includes("p-"); + return ( +
+
+
+ {renderMarkdown(term.content)} +
+
+
+ { + handleAgreementChange(selectedTermId, true); + setSelectedTermId(null); + }} + className="w-full t:w-[526px] h-[48px] px-[16px] py-[12px] mt-[20px] t:mt-[24px] shrink-0 font-sans text-[14px] font-semibold leading-[145%] tracking-[-0.014px]" + > + 동의 + + + + + ); + } + return (
@@ -62,15 +294,18 @@ const TermsAgreement: React.FC = ({ onNext }) => { {TERMS_DATA.map((term) => (
- handleAgreementChange(term.id, !agreements[term.id]) - } + className="flex items-center justify-between w-full gap-[12px]" > - + setSelectedTermId(term.id)} + > {term.label} -
+
handleAgreementChange(term.id, !agreements[term.id])} + > Date: Tue, 17 Feb 2026 01:36:31 +0900 Subject: [PATCH 03/36] =?UTF-8?q?style:=20=ED=97=A4=EB=8D=94=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 55 +++++--------------------------- src/components/layout/Header.tsx | 2 +- 3 files changed, 10 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 4a1ad1e..6732074 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/react-dom": "^19", "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.9.19", "eslint": "^9", "eslint-config-next": "^16.1.6", "postcss": "^8.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee7cf01..c0f67ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + baseline-browser-mapping: + specifier: ^2.9.19 + version: 2.9.19 eslint: specifier: ^9 version: 9.39.1(jiti@2.6.1) @@ -239,105 +242,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -404,28 +391,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.1.6': resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} @@ -484,42 +467,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -591,28 +568,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.17': resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.17': resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.17': resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.17': resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} @@ -769,49 +742,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -970,8 +935,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.25: - resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true brace-expansion@1.1.12: @@ -1641,28 +1606,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -2979,7 +2940,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.25: {} + baseline-browser-mapping@2.9.19: {} brace-expansion@1.1.12: dependencies: @@ -2996,7 +2957,7 @@ snapshots: browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.25 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001754 electron-to-chromium: 1.5.249 node-releases: 2.0.27 @@ -3865,7 +3826,7 @@ snapshots: dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.8.25 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001754 postcss: 8.4.31 react: 19.2.0 diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index fb1ad99..ecb2bc3 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -32,7 +32,7 @@ export default function Header() { const [isSearchOpen, setIsSearchOpen] = useState(false); return (
-
+
{/*로고 + 메뉴*/}
From ff0d0c5502f9b86af71a19ce7410099a417a1894 Mon Sep 17 00:00:00 2001 From: bini0918 <165636716+bini0918@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:39:57 +0900 Subject: [PATCH 04/36] =?UTF-8?q?style:=20=ED=97=A4=EB=8D=94=EC=97=90=20?= =?UTF-8?q?=EC=96=91=EC=98=86=20=EC=97=AC=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/Header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ecb2bc3..6d8f330 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -32,7 +32,7 @@ export default function Header() { const [isSearchOpen, setIsSearchOpen] = useState(false); return (
-
+
{/*로고 + 메뉴*/}
@@ -75,7 +75,7 @@ export default function Header() { {/*아이콘*/} -
+
From 5146a9513d6f8b8cd31b4069898d3330c9ded964 Mon Sep 17 00:00:00 2001 From: bini0918 <165636716+bini0918@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:11:28 +0900 Subject: [PATCH 09/36] =?UTF-8?q?style:=20=EC=B1=85=EC=9D=B4=EC=95=BC?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=91=EC=84=B1=20=EC=83=81=EB=8B=A8=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/stories/new/page.tsx | 60 ++++++++++++----------- src/components/layout/BookSelectModal.tsx | 2 +- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/app/(main)/stories/new/page.tsx b/src/app/(main)/stories/new/page.tsx index a87875f..5d09d79 100644 --- a/src/app/(main)/stories/new/page.tsx +++ b/src/app/(main)/stories/new/page.tsx @@ -42,35 +42,39 @@ function StoryNewContent() { return (
- {/* 책이야기 > 글 작성하기 */} - {/* 모바일: 전체 너비 선 */} -
-
-
전체
-
- next + {/* 책이야기 > 글 작성하기 - 모달 열리면 숨김 */} + {!isBookSelectModalOpen && ( + <> + {/* 모바일: 전체 너비 선 */} +
+
+
전체
+
+ next +
+
글 작성하기
+
-
글 작성하기
-
-
- {/* 태블릿/데스크탑: max-w 안에서 선 */} -
-
전체
-
- next -
-
글 작성하기
-
+ {/* 태블릿/데스크탑: max-w 안에서 선 */} +
+
전체
+
+ next +
+
글 작성하기
+
+ + )} {/* 메인 콘텐츠 영역 */}
{/* 책 선택하기 박스 */} diff --git a/src/components/layout/BookSelectModal.tsx b/src/components/layout/BookSelectModal.tsx index e50e8b7..81e37ab 100644 --- a/src/components/layout/BookSelectModal.tsx +++ b/src/components/layout/BookSelectModal.tsx @@ -83,7 +83,7 @@ export default function BookSelectModal({ return (
{/* 배경 오버레이 - 태블릿/데스크탑에서만 */} From 7747f8278000ef7554a89a8dfc2b41ee7a7e508c Mon Sep 17 00:00:00 2001 From: bini0918 <165636716+bini0918@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:31:03 +0900 Subject: [PATCH 10/36] =?UTF-8?q?style:=20=EA=B2=80=EC=83=89=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/BookSelectModal.tsx | 134 +++++++++++----------- src/components/layout/SearchModal.tsx | 44 +++---- 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/src/components/layout/BookSelectModal.tsx b/src/components/layout/BookSelectModal.tsx index 81e37ab..a2cd8e8 100644 --- a/src/components/layout/BookSelectModal.tsx +++ b/src/components/layout/BookSelectModal.tsx @@ -94,79 +94,77 @@ export default function BookSelectModal({ className="bg-background w-full h-full t:rounded-lg t:shadow-lg t:max-w-[1121px] t:max-h-[748px] t:h-auto overflow-hidden flex flex-col d:px-10 py-0 t:py-6 d:py-6" onClick={(e) => e.stopPropagation()} > - - - {/* 검색창 */} -
-
-
- 검색 -
-
- setSearchValue(e.target.value)} - onKeyDown={handleKeyDown} - className="w-full h-full bg-transparent text-Gray-7 subhead_3 t:headline_3 placeholder:text-Gray-4 focus:outline-none pr-10" - autoFocus - /> - {searchValue && ( - - )} -
+ {/* 검색창 */} +
+
+
+ 검색 +
+
+ setSearchValue(e.target.value)} + onKeyDown={handleKeyDown} + className="w-full h-full bg-transparent text-Gray-7 subhead_3 t:headline_3 placeholder:text-Gray-4 focus:outline-none pr-10" + autoFocus + /> + {searchValue && ( + + )}
+
- {/* 검색 결과 */} -
-

- 총 {searchResults.length}개의 - 검색결과가 있습니다. -

-
- {searchResults.map((result) => ( - - setLikedResults((prev) => ({ ...prev, [result.id]: liked })) - } - onPencilClick={() => { - onSelect(result.id); - onClose(); - }} - onCardClick={() => { - onSelect(result.id); - onClose(); - }} - /> - ))} -
+ {/* 검색 결과 */} +
+

+ 총 {searchResults.length}개 + 의 검색결과가 있습니다. +

+
+ {searchResults.map((result) => ( + + setLikedResults((prev) => ({ ...prev, [result.id]: liked })) + } + onPencilClick={() => { + onSelect(result.id); + onClose(); + }} + onCardClick={() => { + onSelect(result.id); + onClose(); + }} + /> + ))}
+
); } diff --git a/src/components/layout/SearchModal.tsx b/src/components/layout/SearchModal.tsx index 8c73f50..10a93ae 100644 --- a/src/components/layout/SearchModal.tsx +++ b/src/components/layout/SearchModal.tsx @@ -86,8 +86,8 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) { className="fixed left-0 right-0 z-50 bg-primary-1 border-b border-white/20" style={{ top: `${topOffset}px` }} > -
-
+
+
-
-
+
+
{/* 오늘의 추천 책 */} -
-

오늘의 추천 책

-
- {recommendedBooks.slice(0, 4).map((book, index) => ( -
- - setLikedBooks((prev) => ({ ...prev, [book.id]: liked })) - } - /> -
- ))} +
+
+

오늘의 추천 책

+
+ {recommendedBooks.slice(0, 4).map((book, index) => ( +
+ + setLikedBooks((prev) => ({ ...prev, [book.id]: liked })) + } + /> +
+ ))} +
-
+
Date: Tue, 17 Feb 2026 02:34:17 +0900 Subject: [PATCH 11/36] =?UTF-8?q?style:=20=EA=B2=80=EC=83=89=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 29 +++++++++++++++++++++++++++ src/components/layout/SearchModal.tsx | 4 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 604a790..086e1a3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -146,3 +146,32 @@ body { --breakpoint-t: 768px; --breakpoint-d: 1440px; } + +/* 애니메이션 */ +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slide-down { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@utility animate-fade-in { + animation: fade-in 0.2s ease-out; +} + +@utility animate-slide-down { + animation: slide-down 0.3s ease-out; +} diff --git a/src/components/layout/SearchModal.tsx b/src/components/layout/SearchModal.tsx index 10a93ae..92f5c0d 100644 --- a/src/components/layout/SearchModal.tsx +++ b/src/components/layout/SearchModal.tsx @@ -76,14 +76,14 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) { <> {/* 배경 블러 */}
{/* 모달 */}
From cd7be42d457709f0da8c03a0809d6aa1c5602770 Mon Sep 17 00:00:00 2001 From: bini0918 <165636716+bini0918@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:50:55 +0900 Subject: [PATCH 12/36] =?UTF-8?q?style:=20=EC=B1=85=EC=9D=B4=EC=95=BC?= =?UTF-8?q?=EA=B8=B0=EC=B9=B4=EB=93=9C=20=EC=82=AC=EC=9D=B4=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/books/[id]/page.tsx | 97 ++++++++++--------- .../base-ui/Search/search_bookresult.tsx | 44 ++++----- 2 files changed, 70 insertions(+), 71 deletions(-) diff --git a/src/app/(main)/books/[id]/page.tsx b/src/app/(main)/books/[id]/page.tsx index c40cf12..f2b6472 100644 --- a/src/app/(main)/books/[id]/page.tsx +++ b/src/app/(main)/books/[id]/page.tsx @@ -3,8 +3,8 @@ import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import SearchBookResult from "@/components/base-ui/Search/search_bookresult"; -import BookStoryCard from "@/components/base-ui/BookStory/bookstory_card"; import { DUMMY_STORIES } from "@/data/dummyStories"; +import BookStoryCardLarge from "@/components/base-ui/BookStory/bookstory_card_large"; export default function BookDetailPage() { const params = useParams(); @@ -23,58 +23,61 @@ export default function BookDetailPage() { // 관련된 책 이야기들 (더미 데이터에서 필터링) const relatedStories = DUMMY_STORIES.filter( - (story) => story.bookTitle === bookData.title + (story) => story.bookTitle === bookData.title, ); return ( -
-

- 도서 선택 {bookData.title} 중 -

+
+
+

+ 도서 선택 {bookData.title} 중 +

- {/* 선택한 책 카드 */} -
- { - router.push(`/stories/new?bookId=${bookId}`); - }} - /> -
+ {/* 선택한 책 카드 */} +
+ { + router.push(`/stories/new?bookId=${bookId}`); + }} + /> +
- {/* 책이야기 */} -
-

- 책이야기 {relatedStories.length} -

-
+ {/* 책이야기 */} +
+

+ 책이야기{" "} + {relatedStories.length} +

+
- {/* 책 이야기 카드 */} -
- {relatedStories.map((story) => ( -
router.push(`/stories/${story.id}`)} - className="cursor-pointer" - > - -
- ))} + {/* 책 이야기 카드 */} +
+ {relatedStories.map((story) => ( +
router.push(`/stories/${story.id}`)} + className="cursor-pointer" + > + +
+ ))} +
); diff --git a/src/components/base-ui/Search/search_bookresult.tsx b/src/components/base-ui/Search/search_bookresult.tsx index f1b740c..2809316 100644 --- a/src/components/base-ui/Search/search_bookresult.tsx +++ b/src/components/base-ui/Search/search_bookresult.tsx @@ -1,5 +1,5 @@ -'use client'; -import Image from 'next/image'; +"use client"; +import Image from "next/image"; type SearchBookResultProps = { imgUrl?: string; // 없으면 booksample.svg @@ -27,20 +27,20 @@ export default function SearchBookResult({ onPencilClick, onCardClick, - className = '', + className = "", }: SearchBookResultProps) { - const coverSrc = imgUrl && imgUrl.length > 0 ? imgUrl : '/booksample.svg'; + const coverSrc = imgUrl && imgUrl.length > 0 ? imgUrl : "/booksample.svg"; const clippedDetail = - detail.length > 500 ? detail.slice(0, 500) + '...' : detail; + detail.length > 500 ? detail.slice(0, 500) + "..." : detail; return (
-

- {title} -

-

- {author} -

+

{title}

+

{author}

@@ -78,7 +74,7 @@ export default function SearchBookResult({ className="w-[24px] h-[24px] shrink-0" > + > + +
); } From 86ab527230cfd4021ee10424733b3dbba50bbdb9 Mon Sep 17 00:00:00 2001 From: bini0918 <165636716+bini0918@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:03:02 +0900 Subject: [PATCH 13/36] =?UTF-8?q?style:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/groups/[id]/notice/page.tsx | 142 +++++++++++++++++++++------- 1 file changed, 106 insertions(+), 36 deletions(-) diff --git a/src/app/groups/[id]/notice/page.tsx b/src/app/groups/[id]/notice/page.tsx index 31cd83e..99d8625 100644 --- a/src/app/groups/[id]/notice/page.tsx +++ b/src/app/groups/[id]/notice/page.tsx @@ -1,54 +1,60 @@ -'use client'; +"use client"; -import { useParams, useRouter } from 'next/navigation'; -import Image from 'next/image'; -import NoticeItem from '@/components/base-ui/Group/notice_item'; +import { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import Image from "next/image"; +import NoticeItem from "@/components/base-ui/Group/notice_item"; export default function GroupNoticePage() { const params = useParams(); const router = useRouter(); const groupId = params.id as string; - + // TODO: 실제 관리자 여부는 API로 확인 const isAdmin = true; // true: 관리자, false: 일반회원 + // 페이지네이션 상태 + const [currentPage, setCurrentPage] = useState(1); + const totalPages = 5; + // 더미 데이터 const pinnedNotices = [ { id: 1, - title: '긁적긁적 독서 모임 공지사항', - content: '이번 주 모임은 정상적으로 진행됩니다. 많은 참여 부탁드립니다.', - date: '2025.01.15', + title: "긁적긁적 독서 모임 공지사항", + content: "이번 주 모임은 정상적으로 진행됩니다. 많은 참여 부탁드립니다.", + date: "2025.01.15", }, { id: 2, - title: '새로운 책 추천 받습니다', - content: '이번 달 읽을 책을 추천해주세요. 추천하신 책 중에서 선정하겠습니다.', - date: '2025.01.10', + title: "새로운 책 추천 받습니다", + content: + "이번 달 읽을 책을 추천해주세요. 추천하신 책 중에서 선정하겠습니다.", + date: "2025.01.10", }, ]; const notices = [ { id: 3, - title: '1월 독서 후기 공유', - content: '1월에 읽으신 책들의 후기를 공유해주세요.', - date: '2025.01.05', - tags: ['vote'] as const, + title: "1월 독서 후기 공유", + content: "1월에 읽으신 책들의 후기를 공유해주세요.", + date: "2025.01.05", + tags: ["vote"] as const, }, { id: 4, - title: '모임 장소 변경 안내', - content: '다음 모임부터 장소가 변경됩니다. 자세한 내용은 확인해주세요.', - date: '2024.12.28', - tags: ['vote', 'meeting'] as const, + title: "모임 장소 변경 안내", + content: "다음 모임부터 장소가 변경됩니다. 자세한 내용은 확인해주세요.", + date: "2024.12.28", + tags: ["vote", "meeting"] as const, }, { id: 5, - title: '연말 모임 안내', - content: '연말 특별 모임을 준비했습니다. 많은 참여 부탁드립니다.', - date: '2024.12.20', - tags: ['meeting'] as const, + title: "연말 모임 안내", + content: "연말 특별 모임을 준비했습니다. 많은 참여 부탁드립니다.", + date: "2024.12.20", + tags: ["meeting"] as const, }, ]; @@ -95,22 +101,86 @@ export default function GroupNoticePage() { ))}
- {/* 관리자일 때만 공지사항 작성 버튼 표시 */} - {isAdmin && ( -
- + + {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + -
+ ))} + + +
+ + {/* 관리자일 때만 공지사항 작성 버튼 표시 */} + {isAdmin && ( + )}
); From 524c1ba2e24c6d65d36e311579ec07273b8c1e01 Mon Sep 17 00:00:00 2001 From: bini0918 <165636716+bini0918@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:08:48 +0900 Subject: [PATCH 14/36] =?UTF-8?q?style:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/groups/[id]/notice/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/groups/[id]/notice/page.tsx b/src/app/groups/[id]/notice/page.tsx index 99d8625..f893ee5 100644 --- a/src/app/groups/[id]/notice/page.tsx +++ b/src/app/groups/[id]/notice/page.tsx @@ -102,7 +102,7 @@ export default function GroupNoticePage() {
{/* 페이지네이션 */} -
+
{/* 페이지네이션 */} -
+
-
- {/* 구분선 */}
- -
+ +
{/* 제목 및 내용 입력 영역 */}
-

공지사항 작성

- +

+ 공지사항 작성 +

+ {/* 선택된 책 표시 */} {selectedBook && (
@@ -157,215 +143,221 @@ export default function NewNoticePage() { )} {/* 입력 박스 */} -
- {/* 제목 */} -
- setTitle(e.target.value)} - placeholder="제목을 입력해주세요." - className="w-full bg-transparent outline-none text-Gray-7 subhead_4_1 placeholder:text-Gray-3" - /> -
- - {/* 내용 */} -
-