Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/base-ui/Join/JoinLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// JoinLayout.tsx

import React from "react";

interface JoinLayoutProps {
Expand All @@ -10,7 +12,12 @@ const JoinLayout: React.FC<JoinLayoutProps> = ({ children }) => {
className="flex flex-col items-center justify-center w-full min-h-screen pb-[75px] bg-center bg-cover bg-no-repeat"
style={{ backgroundImage: `url('/background.png')` }}
>
<div className="flex flex-col items-center w-[766px] px-[56px] py-[99px] gap-[100px] rounded-[8px] bg-White">
{/*
Container Layout Rules:
- Mobile: Fixed w-[352px], Fluid px-[24px]
- Desktop: Fixed w-[766px], Strict Padding px-[56px] py-[99px]
*/}
<div className="relative flex flex-col items-center w-[352px] h-[720px] px-[24px] md:w-[766px] md:h-auto md:min-h-0 md:px-[56px] md:py-[99px] md:gap-0 rounded-xl bg-white shadow-lg mx-auto">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

모바일 뷰에서 h-[720px]로 고정 높이를 사용하고 계신데, 이 경우 화면이 작은 모바일 기기나 사용자가 시스템 폰트 크기를 키웠을 때 콘텐츠가 잘리거나 레이아웃이 깨질 위험이 있습니다. min-h-[720px]를 사용하시거나, 높이를 고정하지 않고 콘텐츠에 따라 유연하게 조절되도록 하는 것이 더 안전한 방법일 것 같습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 바꾸면 더 좋을듯해요!

{children}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// c:\Users\shinwookKang\Desktop\CheckMo\FE\src\components\base-ui\Join\steps\EmailVerification\EmailVerification.tsx

"use client";

import React from "react";
import JoinHeader from "../../JoinHeader";
import JoinButton from "../../JoinButton";
import JoinInput from "../../JoinInput";
import { useEmailVerification } from "../useEmailVerification";
import Toast from "@/components/common/Toast";

interface EmailVerificationProps {
onNext?: () => void;
onNext: () => void;
}

const EmailVerification: React.FC<EmailVerificationProps> = ({ onNext }) => {
Expand All @@ -22,16 +25,18 @@ const EmailVerification: React.FC<EmailVerificationProps> = ({ onNext }) => {
isVerified,
handleVerify,
showToast,
setShowToast,
isToastVisible,
formatTime,
} = useEmailVerification();

return (
<div className="relative flex flex-col items-center w-[766px] px-[56px] py-[99px] bg-white rounded-[8px]">
<div className="relative flex flex-col items-center mx-auto w-full max-w-[766px] bg-white rounded-[8px] px-6 py-10 md:px-[40px] md:py-[60px] lg:px-[56px] lg:py-[99px]">
<JoinHeader title="이메일 인증" />
<div className="flex flex-col w-full mt-[90px] mb-[130px]">
{/* 이메일 입력 섹션 */}
<div className="flex flex-col items-center w-[526px] gap-[30px] mx-auto">

{/* Content Wrapper: Mobile(mt-10 mb-10) -> Tablet(mt-[60px] mb-[80px]) -> Desktop(mt-[90px] mb-[130px]) */}
<div className="flex flex-col w-full mt-10 mb-10 md:mt-[60px] md:mb-[80px] lg:mt-[90px] lg:mb-[130px]">
{/* Email Input Section */}
<div className="flex flex-col items-center w-full max-w-[526px] gap-[30px] mx-auto">
<JoinInput
label="이메일"
type="email"
Expand All @@ -44,7 +49,7 @@ const EmailVerification: React.FC<EmailVerificationProps> = ({ onNext }) => {
<button
onClick={startTimer}
disabled={!isEmailValid}
className={`flex justify-center items-center w-[284px] h-[48px] rounded-[8px] text-[14px] font-semibold leading-[145%] tracking-[-0.014px] ${
className={`flex justify-center items-center w-full max-w-[284px] h-[48px] rounded-[8px] text-[14px] font-semibold leading-[145%] tracking-[-0.014px] ${
isEmailValid
? "bg-[#BBAA9B] text-[#FFF]"
: "bg-[#DADADA] text-[#8D8D8D]"
Expand All @@ -63,8 +68,8 @@ const EmailVerification: React.FC<EmailVerificationProps> = ({ onNext }) => {
</div>
</div>

{/* 인증번호 입력 섹션 */}
<div className="flex flex-col items-center w-[526px] gap-[30px] mx-auto mt-[53px]">
{/* Verification Code Section */}
<div className="flex flex-col items-center w-full max-w-[526px] gap-[30px] mx-auto mt-[53px]">
<JoinInput
label="인증번호"
type="text"
Expand All @@ -76,7 +81,7 @@ const EmailVerification: React.FC<EmailVerificationProps> = ({ onNext }) => {
<button
onClick={handleVerify}
disabled={!isCodeValid}
className={`flex justify-center items-center w-[284px] h-[48px] rounded-[8px] text-[14px] font-semibold leading-[145%] tracking-[-0.014px] ${
className={`flex justify-center items-center w-full max-w-[284px] h-[48px] rounded-[8px] text-[14px] font-semibold leading-[145%] tracking-[-0.014px] ${
isCodeValid
? "bg-[#BBAA9B] text-[#FFF]"
: "bg-[#DADADA] text-[#8D8D8D]"
Expand All @@ -89,15 +94,25 @@ const EmailVerification: React.FC<EmailVerificationProps> = ({ onNext }) => {
</button>
</div>
</div>
{/* 하단 버튼 (임시로 클릭 시 다음 단계 이동 동작 연결) */}
<JoinButton onClick={onNext} disabled={!isVerified}>

<JoinButton
onClick={onNext}
disabled={!isVerified}
className="w-full md:w-[526px]"
>
다음
</JoinButton>

{showToast && (
<Toast
message="인증이 완료되었습니다."
onClose={() => setShowToast(false)}
/>
<div
className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 inline-flex justify-center items-center h-auto py-4 px-8 md:h-[88px] md:px-[138px] bg-[#31111D99] rounded-[24px] backdrop-blur-[1px] transition-opacity duration-300 ${
isToastVisible ? "opacity-100" : "opacity-0"
}`}
>
<span className="text-[#FFF] text-[16px] md:text-[18px] font-medium leading-[135%] tracking-[-0.018px] whitespace-nowrap">
인증이 완료되었습니다.
</span>
</div>
)}
Comment on lines 106 to 116

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

인증 완료 시 보여주는 Toast UI를 이 컴포넌트 내에 직접 구현하셨네요. 기존에 components/common/Toast 컴포넌트를 사용하고 계셨는데, 이를 대체한 특별한 이유가 있으신가요? 현재 구현은 EmailVerification 컴포넌트에 종속적이라 다른 곳에서 재사용하기 어렵습니다. 또한, 애니메이션을 위한 타이머 로직이 useEmailVerification 훅에 포함되어 있어 UI와 로직이 분리되는 효과가 줄어듭니다. 기존 Toast 컴포넌트를 확장하여 사용하시거나, 이 새로운 Toast를 별도의 재사용 가능한 컴포넌트로 분리하는 것을 고려해보시면 좋겠습니다.

</div>
);
Expand Down
82 changes: 60 additions & 22 deletions src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,99 @@
"use client";

import React from "react";
import JoinHeader from "../../JoinHeader";
import JoinButton from "../../JoinButton";
import JoinInput from "../../JoinInput";
import { usePasswordEntry } from "./usePasswordEntry";

interface PasswordEntryProps {
onNext?: () => void;
onNext: () => void;
}

const PasswordEntry: React.FC<PasswordEntryProps> = ({ onNext }) => {
const {
password,
confirmPassword,
isValid,
isComplexityValid,
isMatch,
handlePasswordChange,
handleConfirmChange,
} = usePasswordEntry();

return (
<div className="relative flex flex-col items-center w-[766px] px-[56px] py-[99px] bg-white rounded-[8px]">
<div className="relative flex flex-col items-center mx-auto w-full max-w-[766px] bg-white rounded-[8px] px-6 py-10 md:px-[40px] md:py-[60px] lg:px-[56px] lg:py-[99px]">
<JoinHeader title="비밀번호 입력" />

<div className="flex flex-col w-full mt-[120px] mb-[306px]">
{/* Main Container for Inputs */}
<div className="flex flex-col items-start w-[526px] gap-[13px] mx-auto">
{/* 비밀번호 입력 필드 (커스텀 라벨) */}
<div className="flex flex-col w-full gap-[13px]">
<div className="flex flex-row items-center gap-2">
{/* Content Wrapper: Mobile(mt-10 mb-10) -> Tablet(mt-[60px] mb-[80px]) -> Desktop(mt-[90px] mb-[130px]) */}
<div className="flex flex-col w-full mt-10 mb-10 md:mt-[60px] md:mb-[80px] lg:mt-[90px] lg:mb-[130px]">
<div className="flex flex-col items-center w-full max-w-[526px] gap-[30px] mx-auto">
{/* Password Input Section */}
<div className="flex flex-col w-full gap-2">
{/* Custom Label Layout for Password */}
<div className="flex flex-wrap items-end gap-2 mb-1">
<span className="text-[#7B6154] font-sans text-[20px] font-semibold leading-[135%] tracking-[-0.02px]">
비밀번호
</span>
<span className="text-[#8D8D8D] text-[12px] font-medium leading-[145%] tracking-[-0.012px]">
<span className="hidden lg:inline text-[#8D8D8D] text-[12px] font-medium leading-[145%] tracking-[-0.012px] mb-[3px]">
비밀번호는 6-12자, 영어 최소 1자 이상, 특수문자 최소 1자 이상
</span>
</div>

<JoinInput
type="password"
placeholder="비밀번호"
placeholder="비밀번호를 입력해주세요"
value={password}
onChange={handlePasswordChange}
className="border-[#EAE5E2] placeholder-[#BBB] text-[14px] font-normal"
className={`border-[#EAE5E2] placeholder-[#BBB] text-[14px] font-normal ${
!isComplexityValid && password.length > 0
? "border-red-500"
: ""
}`}
hideLabel // Using custom label above
/>

{!isComplexityValid && password.length > 0 && (
<span className="text-red-500 text-[12px] ml-1">
비밀번호 형식이 올바르지 않습니다.
</span>
)}
</div>

{/* 비밀번호 확인 입력 필드 */}
<JoinInput
type="password"
label="비밀번호 확인"
hideLabel
placeholder="비밀번호 확인"
value={confirmPassword}
onChange={handleConfirmChange}
className="border-[#EAE5E2] placeholder-[#BBB] text-[14px] font-normal"
/>
{/* Confirm Password Input Section */}
<div className="flex flex-col w-full gap-2">
{/* Custom Label Layout for Confirm Password */}
<div className="flex flex-wrap items-end gap-2 mb-1">
<span className="text-[#7B6154] font-sans text-[20px] font-semibold leading-[135%] tracking-[-0.02px]">
비밀번호 확인
</span>
</div>

<JoinInput
type="password"
placeholder="비밀번호를 다시 입력해주세요"
value={confirmPassword}
onChange={handleConfirmChange}
className={`border-[#EAE5E2] placeholder-[#BBB] text-[14px] font-normal ${
!isMatch && confirmPassword.length > 0 ? "border-red-500" : ""
}`}
hideLabel
/>

{!isMatch && confirmPassword.length > 0 && (
<span className="text-red-500 text-[12px] ml-1">
비밀번호가 일치하지 않습니다.
</span>
)}
</div>
</div>
</div>

<JoinButton onClick={onNext} disabled={!isValid}>
<JoinButton
onClick={onNext}
disabled={!isValid}
className="w-full md:w-[526px]"
>
다음
</JoinButton>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { useState, useEffect } from "react";
import { useState } from "react";

export const usePasswordEntry = () => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [isValid, setIsValid] = useState(false);

useEffect(() => {
// 6-12자, 영문 최소 1자, 특수문자 최소 1자
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^&*]).{6,12}$/;
const isPasswordValid = passwordRegex.test(password);
const isMatch = password === confirmPassword;
// 6-12자, 영문 최소 1자, 특수문자 최소 1자
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^&*]).{6,12}$/;

setIsValid(isPasswordValid && isMatch && password.length > 0);
}, [password, confirmPassword]);
// Derived State (No useEffect)
const isComplexityValid = passwordRegex.test(password);
const isMatch = password === confirmPassword;
const isValid =
isComplexityValid &&
isMatch &&
password.length > 0 &&
confirmPassword.length > 0;
Comment on lines +8 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useEffect를 사용하지 않고 파생 상태(derived state)로 유효성 검사 로직을 변경하신 점이 매우 좋습니다. 코드가 더 선언적이고 예측 가능해졌습니다. 훌륭한 리팩토링입니다!


const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
Expand All @@ -26,6 +28,8 @@ export const usePasswordEntry = () => {
password,
confirmPassword,
isValid,
isComplexityValid,
isMatch,
handlePasswordChange,
handleConfirmChange,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ const InterestCategorySelector: React.FC<InterestCategorySelectorProps> = ({
}) => {
return (
<div className="flex flex-col w-full gap-[16px]">
<div className="flex flex-row items-center gap-[10px]">
<span className="text-[#7B6154] font-sans text-[20px] font-semibold leading-[135%] tracking-[-0.02px]">
<div className="flex flex-col md:flex-row items-center justify-center md:justify-start gap-2 md:gap-[10px]">
{/* Title: Hidden on Mobile (shown in Header instead), Visible on Desktop */}
<span className="hidden md:block text-[#7B6154] font-sans text-[20px] font-semibold leading-[135%] tracking-[-0.02px]">
관심 카테고리
</span>
{/* Subtitle: Visible & Centered on Mobile */}
<div className="flex h-[27px] p-[10px] justify-center items-center gap-[10px]">
<span className="text-[#8D8D8D] font-sans text-[14px] font-semibold leading-[145%] tracking-[-0.014px]">
(최소 1개, 최대 6개 선택)
</span>
</div>
</div>
<div className="flex flex-wrap gap-[12px]">

{/* Grid for Mobile (3 cols), Flex for Desktop */}
<div className="grid grid-cols-3 gap-2 w-full md:flex md:flex-wrap md:gap-[12px]">
{INTEREST_CATEGORIES.map((category) => {
const isSelected = selectedInterests.includes(category);
return (
<button
key={category}
onClick={() => onToggle(category)}
className={`w-[122px] h-[44px] flex justify-center items-center rounded-[400px] text-[14px] leading-[145%] tracking-[-0.014px] transition-colors ${
className={`w-full md:w-[122px] h-[44px] flex justify-center items-center rounded-[400px] text-[14px] leading-[145%] tracking-[-0.014px] transition-colors ${
isSelected
? "bg-[#A19182] border border-[#D2C5B6] text-[#FFF] font-medium"
: "bg-[#F9F7F6] border border-[#D2C5B6] text-[#5C5C5C] font-normal"
Expand Down
Loading