Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] #50 반려견 등록 페이지 기능 완성 #96

Merged
merged 10 commits into from
Nov 27, 2024
4 changes: 2 additions & 2 deletions src/components/GenderSelectButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ interface GenderSelectButtonProps {

export default function GenderSelectButton({ gender, isActive, onClick }: GenderSelectButtonProps) {
return (
<S.GenderBtn isActive={isActive} onClick={onClick}>
<S.GenderIcon isActive={isActive} src={gender === 'male' ? Male : Female} alt='성별' />
<S.GenderBtn $isActive={isActive} onClick={onClick}>
<S.GenderIcon $isActive={isActive} src={gender === 'male' ? Male : Female} alt='성별' />
<Typo17 weight={isActive ? '700' : '400'}>{gender === 'male' ? '남' : '여'}</Typo17>
</S.GenderBtn>
)
Expand Down
10 changes: 5 additions & 5 deletions src/components/GenderSelectButton/styles.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { styled } from 'styled-components'
export const GenderBtn = styled.button<{ isActive: boolean }>`
border: solid 2px ${({ isActive, theme }) => (isActive ? theme.colors.brand.darken : theme.colors.grayscale.gc_1)};
export const GenderBtn = styled.button<{ $isActive: boolean }>`
border: solid 2px ${({ $isActive, theme }) => ($isActive ? theme.colors.brand.darken : theme.colors.grayscale.gc_1)};
border-radius: 8px;
width: auto;
height: 102px;
color: ${({ isActive, theme }) => (isActive ? theme.colors.brand.darken : theme.colors.grayscale.font_3)};
color: ${({ $isActive, theme }) => ($isActive ? theme.colors.brand.darken : theme.colors.grayscale.font_3)};

display: flex;
flex-direction: column;
Expand All @@ -13,9 +13,9 @@ export const GenderBtn = styled.button<{ isActive: boolean }>`
gap: 0.2rem;
`

export const GenderIcon = styled.img<{ isActive: boolean }>`
export const GenderIcon = styled.img<{ $isActive: boolean }>`
filter: ${props =>
props.isActive
props.$isActive
? 'invert(12%) sepia(70%) saturate(924%) hue-rotate(351deg) brightness(96%) contrast(97%)'
: 'invert(31%) sepia(4%) saturate(61%) hue-rotate(332deg) brightness(99%) contrast(97%)'};
`
3 changes: 1 addition & 2 deletions src/components/Toast/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// components/Toast/index.tsx
import * as S from './styles'
import { useEffect } from 'react'
import { useToastStore } from '~/stores/toastStore'
Expand All @@ -16,7 +15,7 @@ export default function Toast() {
}, [isVisible])

return (
<S.ToastWrapper isVisible={isVisible}>
<S.ToastWrapper $isVisible={isVisible}>
<S.Toast>{content}</S.Toast>
</S.ToastWrapper>
)
Expand Down
6 changes: 3 additions & 3 deletions src/components/Toast/styles.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { styled } from 'styled-components'

export const ToastWrapper = styled.div<{ isVisible: boolean }>`
export const ToastWrapper = styled.div<{ $isVisible: boolean }>`
position: absolute;
top: -50px;

width: 100%;
display: flex;
justify-content: center;

visibility: ${({ isVisible }) => (isVisible ? 'visiblie' : 'hidden')};
opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
visibility: ${({ $isVisible }) => ($isVisible ? 'visiblie' : 'hidden')};
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
transition:
opacity 0.3s ease,
visibility 0.3s ease;
Expand Down
119 changes: 119 additions & 0 deletions src/data/breeds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"breeds": [
"래브라도 리트리버",
"골든 리트리버",
"저먼 셰퍼드",
"도베르만",
"그레이트 데인",
"버니즈 마운틴 독",
"뉴펀들랜드",
"로트와일러",
"세인트 버나드",
"아이리시 울프하운드",
"잉글리시 마스티프",
"그레이트 피레니즈",
"시베리안 허스키",
"알래스칸 맬러뮤트",
"보더 콜리",
"비글",
"불독",
"차우차우",
"달마시안",
"사모예드",
"시바견",
"웰시코기",
"진도견",
"아키타",
"바셋하운드",
"브리타니",
"콜리",
"잉글리시 세터",
"잉글리시 스프링거 스패니얼",
"벨기에 말리노이즈",
"포인터",
"에어데일 테리어",
"휘펫",
"불 테리어",
"스탠더드 푸들",
"아메리칸 에스키모",
"보더 테리어",
"웨스트하이랜드 화이트 테리어",
"프렌치 불독",
"포메라니안",
"치와와",
"요크셔테리어",
"미니어처 슈나우저",
"미니어처 푸들",
"토이 푸들",
"말티즈",
"시츄",
"닥스훈트",
"비숑 프리제",
"파피용",
"퍼그",
"페키니즈",
"잭 러셀 테리어",
"미니어처 핀셔",
"캐벌리어 킹 찰스 스패니얼",
"보스턴 테리어",
"이탈리안 그레이하운드",
"스코티시 테리어",
"실키 테리어",
"케언 테리어",
"노리치 테리어",
"아프간 하운드",
"살루키",
"바센지",
"차이니즈 샤페이",
"아메리칸 코카 스패니얼",
"잉글리시 코카 스패니얼",
"클럼버 스패니얼",
"필드 스패니얼",
"저먼 와이어헤어드 포인터",
"체서피크 베이 리트리버",
"컬리코티드 리트리버",
"플랫코티드 리트리버",
"아이리시 세터",
"고든 세터",
"올드 잉글리시 시프도그",
"셔틀랜드 시프도그",
"벨기안 시프도그",
"오스트레일리안 캐틀 독",
"핀란드 스피츠",
"케이스혼드",
"티베탄 마스티프",
"불마스티프",
"네아폴리탄 마스티프",
"블러드하운드",
"그레이하운드",
"노르웨이언 엘크하운드",
"아이리시 워터 스패니얼",
"웨일즈 스프링거 스패니얼",
"스탠더드 슈나우저",
"자이언트 슈나우저",
"스코티시 디어하운드",
"맨체스터 테리어",
"노퍽 테리어",
"래빗 닥스훈트",
"롱헤어드 닥스훈트",
"아메리칸 불리",
"버니두들",
"골든두들",
"래브라두들",
"포르투갈 워터 독",
"오스트레일리안 셰퍼드",
"벨기에 터뷰런",
"블랙 러시안 테리어",
"불 마스티프",
"체서피크 베이 리트리버",
"샤페이",
"클럼버 스패니얼",
"컬리 코티드 리트리버",
"댄디 딘몬트 테리어",
"잉글리시 폭스하운드",
"필드 스패니얼",
"핀란드 라프훈드",
"자이언트 슈나우저",
"아이비전 하운드"
]
}
2 changes: 1 addition & 1 deletion src/modals/DatePickerModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function DatePickerModal({ date, setDate }: DatePickerModalProps)

return (
<S.ModalOverlay onClick={close}>
<S.DatePickerModal isExiting={isExiting} onClick={handleModalClick}>
<S.DatePickerModal $isExiting={isExiting} onClick={handleModalClick}>
<S.ConfirmBtn onClick={handleConfirmBtn}>확인</S.ConfirmBtn>
<S.Divider />
<DatePicker
Expand Down
6 changes: 4 additions & 2 deletions src/modals/DatePickerModal/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const slideDown = keyframes`
`

export const ModalOverlay = styled.div`
background-color: rgba(0, 0, 0, 0.4);
z-index: 200;
position: fixed;
top: 0;
left: 0;
Expand All @@ -28,11 +30,11 @@ export const ModalOverlay = styled.div`
align-items: flex-end;
`

export const DatePickerModal = styled.div<{ isExiting: boolean }>`
export const DatePickerModal = styled.div<{ $isExiting: boolean }>`
background-color: white;
width: 100%;

animation: ${({ isExiting }) => (isExiting ? slideDown : slideUp)} 0.3s ease-out;
animation: ${({ $isExiting }) => ($isExiting ? slideDown : slideUp)} 0.3s ease-out;

> div {
padding: 1rem;
Expand Down
5 changes: 3 additions & 2 deletions src/modals/RegisterDogModal/CheckDogProfileSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import Header from '~components/Header/index'
import { Typo24 } from '~components/Typo/index'
import { Profile } from '~components/Profile'
import Tag from '~components/Tag'
import { useModalStore } from '~stores/modalStore'

export default function CheckDogProfileSection() {
const handleClickPrev = () => {}
const { popModal } = useModalStore()

return (
<>
<Header type='sm' onClickPrev={handleClickPrev} prevBtn={true} />
<S.CheckDogProfileSection>
<Header type='sm' onClickPrev={popModal} prevBtn={true} />
<S.ProfileArea>
<S.TypoWrapper>
<Typo24 weight='700'>
Expand Down
7 changes: 6 additions & 1 deletion src/modals/RegisterDogModal/CheckDogProfileSection/styles.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { styled } from 'styled-components'

export const CheckDogProfileSection = styled.div`
z-index: 200;
padding: 180px 20px 24px 20px;
background-color: ${({ theme }) => theme.colors.grayscale.gc_4};
height: 100dvh;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;

display: flex;
flex-direction: column;
Expand Down
64 changes: 44 additions & 20 deletions src/modals/RegisterDogModal/DogProfileDetailSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,66 @@ import GenderSelectButton from '~components/GenderSelectButton'
import { Typo24 } from '~components/Typo/index'
import Check from '~assets/check.svg'
import Header from '~components/Header/index'
import SearchModal from '~modals/SearchModal'
import { useModalStore } from '~stores/modalStore'
import { useDogProfileStore } from '~/stores/dogProfileStore'
import { validateDogDetailProfile } from '~utils/validateDogProfile'
import { useToastStore } from '~stores/toastStore'
import Toast from '~components/Toast'

export default function DogProfileDetailSection() {
const [isNeutered, setIsNeutered] = useState(false)
const [selectedGender, setSelectedGender] = useState<'male' | 'female' | null>(null)
const [weight, setWeight] = useState('')
const { dogProfile, setDogProfile } = useDogProfileStore()
const { pushModal, popModal } = useModalStore()
const { showToast } = useToastStore()

const [displayValue, setDisplayValue] = useState('')
const [inputType, setInputType] = useState('text')

const handleGenderSelect = (gender: 'male' | 'female') => {
setSelectedGender(gender)
setDogProfile({ gender })
}

const onChangeWeightInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (value === '') {
setWeight('')
setDogProfile({ weight: '' })
setDisplayValue('')
return
}

if (/^\d*\.?\d*$/.test(value)) {
const formatted = value.includes('.') ? value.match(/^\d*\.?\d{0,2}/)![0] : value

setWeight(formatted)
setDogProfile({ weight: formatted })
setDisplayValue(inputType === 'number' ? formatted : `${formatted}kg`)
}
}

const handleFocus = () => {
setInputType('number')
setDisplayValue(weight)
setDisplayValue(dogProfile.weight)
}

const handleBlur = () => {
setInputType('text')
if (weight) {
setDisplayValue(`${weight}kg`)
if (dogProfile.weight) {
setDisplayValue(`${dogProfile.weight}kg`)
}
}

const handleClickPrev = () => {}
const handleComfirmClick = () => {
const alertMessage = validateDogDetailProfile(dogProfile)
if (alertMessage) {
showToast(alertMessage)
return
}
console.log('이제 백엔드로 전송')
}

return (
<>
<Header type='sm' onClickPrev={handleClickPrev} prevBtn />
<S.DogProfileDetailSection>
<Header type='sm' onClickPrev={popModal} prevBtn />
<S.TypoWrapper>
<Typo24 weight='700'>
반려견 상세 정보를
Expand All @@ -62,34 +75,45 @@ export default function DogProfileDetailSection() {
<S.GenderSelectBtnWrapper>
<GenderSelectButton
gender='male'
isActive={selectedGender === 'male'}
isActive={dogProfile.gender === 'male'}
onClick={() => handleGenderSelect('male')}
/>
<GenderSelectButton
gender='female'
isActive={selectedGender === 'female'}
isActive={dogProfile.gender === 'female'}
onClick={() => handleGenderSelect('female')}
/>
</S.GenderSelectBtnWrapper>
<S.CheckboxWrapper onClick={() => setIsNeutered(!isNeutered)}>
<S.CheckboxCircle isChecked={isNeutered}>
{isNeutered && <img src={Check} alt='check'></img>}
<S.CheckboxWrapper onClick={() => setDogProfile({ isNeutered: !dogProfile.isNeutered })}>
<S.CheckboxCircle $isChecked={dogProfile.isNeutered}>
{dogProfile.isNeutered && <img src={Check} alt='check'></img>}
</S.CheckboxCircle>
<S.CheckboxLabel isChecked={isNeutered}>중성화 했어요</S.CheckboxLabel>
<S.CheckboxLabel $isChecked={dogProfile.isNeutered}>중성화 했어요</S.CheckboxLabel>
</S.CheckboxWrapper>
</S.GenderBtnArea>
<S.InputArea>
<S.PickerBtn>견종 입력</S.PickerBtn>
<S.PickerBtn onClick={() => pushModal(<SearchModal />)} $hasBreed={!!dogProfile.breed}>
{dogProfile.breed || '견종 입력'}
</S.PickerBtn>
<S.WeightInput
placeholder='몸무게 입력'
placeholder='몸무게 입력 (kg)'
type={inputType}
value={displayValue}
onChange={onChangeWeightInput}
onFocus={handleFocus}
onBlur={handleBlur}
$hasWeight={!!dogProfile.weight}
/>
</S.InputArea>
<ActionButton>확인</ActionButton>
<S.ToastWrapper>
<ActionButton
$bgColor={validateDogDetailProfile(dogProfile) ? 'gc_1' : 'default'}
onClick={handleComfirmClick}
>
확인
</ActionButton>
<Toast />
</S.ToastWrapper>
</S.DogProfileDetailSection>
</>
)
Expand Down
Loading