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

feat: 가격 제안 모달 구현 #106

Merged
merged 7 commits into from
Oct 4, 2023
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
17 changes: 17 additions & 0 deletions src/components/product/PriceOfferModal/PriceOfferModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, Story } from '@storybook/react'
import type { PriceOfferModalProps } from './types'
import { PriceOfferModal } from './index'

export default {
component: PriceOfferModal,
title: 'Product/PriceOfferModal'
} as Meta<PriceOfferModalProps>

const Template: Story<PriceOfferModalProps> = args => (
sonsurim marked this conversation as resolved.
Show resolved Hide resolved
<PriceOfferModal {...args} />
)

export const Default = Template.bind({})
Default.args = {
isOpen: true
}
158 changes: 158 additions & 0 deletions src/components/product/PriceOfferModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Input, Divider, IconButton, SelectBox } from '@offer-ui/react'
import type { ChangeEventHandler, ReactElement } from 'react'
import { useState } from 'react'
import { Styled } from './styled'
import type { OfferForm, PriceOfferModalProps, TradeAreaKeys } from './types'
import { TRADE_METHOD } from '@constants'

const MOCK_CITY = [
shinhyojeong marked this conversation as resolved.
Show resolved Hide resolved
{ code: 1, name: 'Seoul' },
{
code: 2,
name: 'Deajeon'
},
{
code: 3,
name: 'Incheon'
shinhyojeong marked this conversation as resolved.
Show resolved Hide resolved
}
]

export const PriceOfferModal = ({
onClickOffer,
...props
}: PriceOfferModalProps): ReactElement => {
const [offerForm, setOfferForm] = useState<OfferForm>({})
const isTradeDirection = offerForm?.tradeMethod !== 4
const isRequiredFormFilled = offerForm.price && offerForm.tradeMethod
const isDirectionFormFilled =
offerForm?.tradeArea?.city &&
offerForm?.tradeArea?.county &&
offerForm?.tradeArea?.town
const canOffer = isTradeDirection
? isRequiredFormFilled && isDirectionFormFilled
: isRequiredFormFilled

const handleChangePrice: ChangeEventHandler<HTMLInputElement> = e => {
const formattedValue = Number(e.target.value.split(',').join(''))

setOfferForm(prev => ({
...prev,
price: formattedValue
}))
}

const handleChangeTradeMethod: ChangeEventHandler<HTMLFormElement> = e => {
const tradeMethod = Number(e.target.value)
const isTradeParcel = tradeMethod === 4
const nextOfferFrom = {
...offerForm,
tradeMethod
}

if (isTradeParcel) {
delete nextOfferFrom.tradeArea
}

setOfferForm(nextOfferFrom)
}

const handleChangeTradeArea = (name: TradeAreaKeys, value: number): void => {
setOfferForm(prev => ({
...prev,
tradeArea: {
...prev.tradeArea,
[name]: value
}
}))
}

const handleClickOffer = (): void => {
onClickOffer?.(offerForm)
}

return (
<Styled.PriceOfferModal {...props}>
<Styled.Header>
<Styled.CloseIconWrapper>
<IconButton color="grayScale30" icon="close" size={24} />
</Styled.CloseIconWrapper>
<Styled.Title>가격을 제안해볼까요?</Styled.Title>
<Styled.Description>
가격이 마음에 든다면, 연락이 올거에요!
shinhyojeong marked this conversation as resolved.
Show resolved Hide resolved
</Styled.Description>
</Styled.Header>
<Styled.Body>
<div>
<Styled.FormTitle>제안 가격</Styled.FormTitle>
<Input
isPrice
placeholder="제안할 가격을 적어주세요"
value={offerForm.price}
onChange={handleChangePrice}
/>
</div>
<Divider />
<div>
<Styled.FormTitle isMainTitle>거래 방식</Styled.FormTitle>
<Styled.FormRadio
direction="horizontal"
formName="trade-method"
items={TRADE_METHOD}
onChange={handleChangeTradeMethod}
/>
</div>
{isTradeDirection && (
<>
<Divider />
<div>
<Styled.FormTitle>거래 지역</Styled.FormTitle>
<SelectBox
items={MOCK_CITY}
placeholder="선택"
size="medium"
value={offerForm?.tradeArea?.city}
onChange={(item): void =>
handleChangeTradeArea('city', item.code)
}
/>
</div>
<Styled.TradeArea>
<div>
<Styled.FormTitle>시/군/구</Styled.FormTitle>
<SelectBox
items={MOCK_CITY}
placeholder="선택"
size="medium"
value={offerForm?.tradeArea?.county}
onChange={(item): void =>
handleChangeTradeArea('county', item.code)
}
/>
</div>
<div>
<Styled.FormTitle>읍/면/동</Styled.FormTitle>
<SelectBox
items={MOCK_CITY}
placeholder="선택"
size="medium"
value={offerForm?.tradeArea?.town}
onChange={(item): void =>
handleChangeTradeArea('town', item.code)
}
/>
</div>
</Styled.TradeArea>
</>
)}
</Styled.Body>
<div>
<Styled.OfferButton
disabled={!canOffer}
size="large"
onClick={handleClickOffer}>
Offer !
</Styled.OfferButton>
</div>
</Styled.PriceOfferModal>
)
}
99 changes: 99 additions & 0 deletions src/components/product/PriceOfferModal/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { SerializedStyles } from '@emotion/react'
import { css } from '@emotion/react'
import styled from '@emotion/styled'
import { Button, Modal, Radio } from '@offer-ui/react'

const PriceOfferModal = styled(Modal)`
width: 400px;

${({ theme }): SerializedStyles => css`
${theme.mediaQuery.tablet} {
width: 320px;
}

${theme.mediaQuery.mobile} {
width: 100vw;
height: 100vh;
}
`}
`

const Header = styled.div`
text-align: center;
`

const CloseIconWrapper = styled.div`
display: flex;
justify-content: end;

width: 100%;
margin-bottom: 12px;
`

const Title = styled.div`
${({ theme }): string => theme.fonts.headline01B}
`

const Description = styled.div`
${({ theme }): string => theme.fonts.body01R}
`

const Body = styled.div`
display: flex;
flex-direction: column;
gap: 20px;

margin: 20px 0 40px;
`

const Label = styled.label<{ isSubTitle?: boolean }>`
${({ theme, isSubTitle }): string =>
isSubTitle ? theme.fonts.subtitle01B : theme.fonts.body01R};
`

const FormRadio = styled(Radio)`
flex-wrap: wrap;

label > span {
${({ theme }): string => theme.fonts.body02R};
}
`

const FormTitle = styled.p<{ isMainTitle?: boolean }>`
margin-bottom: 8px;

${({ theme, isMainTitle }): SerializedStyles => css`
margin-bottom: ${isMainTitle ? '20px' : '8px'};

${isMainTitle ? theme.fonts.subtitle01B : theme.fonts.body01R}
`};
`

const TradeArea = styled.div`
display: flex;
gap: 8px;

& > div {
width: 100%;
}
`

const OfferButton = styled(Button)`
:disabled {
background-color: ${({ theme }): string => theme.colors.grayScale20};
}
`

export const Styled = {
PriceOfferModal,
Header,
CloseIconWrapper,
Title,
Description,
Body,
Label,
FormTitle,
FormRadio,
TradeArea,
OfferButton
}
15 changes: 15 additions & 0 deletions src/components/product/PriceOfferModal/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ModalProps } from '@offer-ui/react'

export type TradeAreaKeys = 'city' | 'county' | 'town'

export type OfferForm = {
price?: number
tradeMethod?: number
tradeArea?: {
[key in TradeAreaKeys]?: number
}
}

export type PriceOfferModalProps = Omit<ModalProps, 'children'> & {
onClickOffer?(offerForm: OfferForm): void
}
1 change: 1 addition & 0 deletions src/components/product/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ProductField'
export * from './UserProfile'
export * from './PriceOfferCard'
export * from './PriceOfferModal'
2 changes: 1 addition & 1 deletion src/constants/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const TRADE_METHOD: TradeMethod[] = [
},
{
code: 8,
name: '상관없음'
name: '직거래/택배거래'
}
]

Expand Down
4 changes: 2 additions & 2 deletions src/types/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ export type TradeMethodOnDelivery = {
}
export type TradeMethodOnAny = {
code: 8
name: '상관없음'
name: '직거래/택배거래'
}
export type TradeMethodCode = 2 | 4 | 8
export type TradeMethodName = '직거래' | '택배거래' | '상관없음'
export type TradeMethodName = '직거래' | '택배거래' | '직거래/택배거래'
export type TradeMethod =
| TradeMethodOnDirect
| TradeMethodOnDelivery
Expand Down