-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore(apps/web): motion 추가 * chore(packages/ui): styles export 변경 * feat(packages/theme): 색상 추가 * feat(packages/theme): spacing 추가 * feat(apps/web): KeywordChip 컴포넌트 추가 * feat(apps/web): ImageManager 컴포넌트 추가 * feat(apps/web): 주제 설정 페이지 퍼블리싱 * refactor(apps/web): 컴포넌트 분리 및 자잘한 수정 * fix(apps/web): react-hook-form watch를 통한 조건부 렌더링 * feat(apps/web): react-hook-form을 통한 폼 관리 * fix(apps/web): ImageManager 컴포넌트 packages/ui로 이동 예정 * fix(apps/web): ImageManager TypeA css 수정 * fix(apps/web): GradientAnimatedTitle로 수정 * fix(apps/web): GradientAnimatedTitle로 수정 * feat(apps/web): isEmptyStringOrNil 유틸 추가 * fix(apps/web): ImageManager TypeA 제어형으로 사용 가능하도록 변경 * refactor(apps/web): 컴포넌트 분리 * chore(apps/web): px->rem 변경 * fix(app/web): defaultValues 수정 * fix(apps/web): css 수정 * fix(apps/web): placeholder 수정
- Loading branch information
1 parent
f2633ce
commit d1fe2ca
Showing
32 changed files
with
819 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
'use client'; | ||
|
||
import { | ||
TextField, | ||
Label, | ||
Spacing, | ||
RadioCards, | ||
Breadcrumb, | ||
Icon, | ||
Button, | ||
} from '@repo/ui'; | ||
import { ImageManager, MainBreadcrumbItem } from '@web/components/common'; | ||
import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; | ||
import { GradientAnimatedTitle } from './_components/GradientAnimatedTitle/GradientAnimatedTitle'; | ||
import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; | ||
import { useForm, Controller } from 'react-hook-form'; | ||
import { isEmptyStringOrNil } from '@web/utils'; | ||
import { CreateFormValues } from './types'; | ||
import { | ||
REFERENCE_TYPE, | ||
PURPOSE_OPTIONS, | ||
REFERENCE_OPTIONS, | ||
LENGTH_OPTIONS, | ||
} from './constants'; | ||
import * as styles from './pageStyle.css'; | ||
|
||
export default function Create() { | ||
const { watch, control, handleSubmit } = useForm<CreateFormValues>({ | ||
defaultValues: { | ||
topic: '', | ||
purpose: 'INFORMATION', | ||
reference: 'NONE', | ||
newsCategory: '투자', // TODO: 백엔드로부터 받는 데이터 타입으로 수정 | ||
imageUrls: [], // TODO: presigned url 받아서 첨부 | ||
length: 'SHORT', | ||
content: '', | ||
}, | ||
mode: 'onChange', | ||
}); | ||
|
||
const topic = watch('topic'); | ||
const reference = watch('reference'); | ||
|
||
const onSubmit = (data: CreateFormValues) => { | ||
//TODO: 임시 로직. 이런 식으로 요청해야 함 | ||
// // 1. presigned URL 요청 | ||
// const presignedUrls = await fetchPresignedUrls(data.imageUrls); // 🔹 presigned URL 요청 | ||
|
||
// // 2. 파일을 presigned URL로 업로드 | ||
// await Promise.all( | ||
// data.imageUrls.map((file, index) => | ||
// uploadFileToPresignedUrl(presignedUrls[index], file) | ||
// ) | ||
// ); | ||
|
||
const presignedUrls = [ | ||
'https://example.com/image1.jpg', | ||
'https://example.com/image2.jpg', | ||
]; | ||
|
||
const requestData = { | ||
...data, | ||
newsCategory: data.reference === 'NEWS' ? data.newsCategory : null, | ||
imageUrls: data.reference === 'IMAGE' ? presignedUrls : null, | ||
}; | ||
|
||
console.log('폼 데이터:', requestData); | ||
}; | ||
|
||
const isSubmitDisabled = isEmptyStringOrNil(topic); | ||
|
||
return ( | ||
<div className={styles.mainStyle}> | ||
<div className={styles.headerStyle}> | ||
<Breadcrumb> | ||
<Breadcrumb.Item> | ||
<MainBreadcrumbItem href="/create" /> | ||
</Breadcrumb.Item> | ||
</Breadcrumb> | ||
<Button | ||
type="submit" | ||
size="large" | ||
variant="primary" | ||
leftAddon={<Icon name="twinkle" />} | ||
onClick={handleSubmit(onSubmit)} | ||
disabled={isSubmitDisabled} | ||
> | ||
생성하기 | ||
</Button> | ||
</div> | ||
|
||
<Spacing size={80} /> | ||
|
||
<GradientAnimatedTitle>어떤 글을 생성할까요?</GradientAnimatedTitle> | ||
|
||
<AnimatedContainer> | ||
<form className={styles.contentStyle}> | ||
{/* 주제 */} | ||
<section className={styles.sectionStyle}> | ||
<TextField id="topic"> | ||
<TextField.Label variant="required">주제</TextField.Label> | ||
<Controller | ||
name="topic" | ||
control={control} | ||
render={({ field }) => ( | ||
<TextField.Input | ||
{...field} | ||
placeholder="주제를 적어주세요" | ||
maxLength={5000} | ||
/> | ||
)} | ||
/> | ||
</TextField> | ||
</section> | ||
|
||
{/* 목적 */} | ||
<section className={styles.sectionStyle}> | ||
<Label variant="default">목적</Label> | ||
<Controller | ||
name="purpose" | ||
control={control} | ||
render={({ field: { onChange, value } }) => ( | ||
<RadioCards value={value} onChange={onChange} columns={4}> | ||
{PURPOSE_OPTIONS.map(({ value, icon, label }) => ( | ||
<RadioCards.Item | ||
key={value} | ||
value={value} | ||
leftAddon={<RadioCards.Icon name={icon} size={24} />} | ||
> | ||
<RadioCards.Label>{label}</RadioCards.Label> | ||
</RadioCards.Item> | ||
))} | ||
</RadioCards> | ||
)} | ||
/> | ||
</section> | ||
|
||
{/* 생성 방식 */} | ||
<section className={styles.sectionStyle}> | ||
<Label>생성 방식</Label> | ||
<Controller | ||
name="reference" | ||
control={control} | ||
render={({ field: { onChange, value } }) => ( | ||
<RadioCards value={value} onChange={onChange} columns={3}> | ||
{REFERENCE_OPTIONS.map( | ||
({ value, icon, label, description }) => ( | ||
<RadioCards.Item | ||
key={value} | ||
value={value} | ||
leftAddon={<RadioCards.Icon name={icon} size={24} />} | ||
> | ||
<RadioCards.Label>{label}</RadioCards.Label> | ||
<RadioCards.Description> | ||
{description} | ||
</RadioCards.Description> | ||
</RadioCards.Item> | ||
) | ||
)} | ||
</RadioCards> | ||
)} | ||
/> | ||
{reference === REFERENCE_TYPE.IMAGE && ( | ||
<Controller | ||
name="imageUrls" | ||
control={control} | ||
render={({ field: { value, onChange } }) => ( | ||
<ImageManager.TypeA value={value || []} onChange={onChange} /> | ||
)} | ||
/> | ||
)} | ||
</section> | ||
|
||
{/* 조건부 렌더링 섹션들 */} | ||
{reference === REFERENCE_TYPE.NEWS && ( | ||
<section className={styles.sectionStyle}> | ||
<Label variant="required">뉴스 카테고리</Label> | ||
<Controller | ||
name="newsCategory" | ||
control={control} | ||
render={({ field: { value, onChange } }) => ( | ||
<KeywordChipGroup onChange={onChange} defaultValue={value}> | ||
{['투자', '패션', '피트니스', '헬스케어']} | ||
</KeywordChipGroup> | ||
)} | ||
/> | ||
</section> | ||
)} | ||
|
||
{/* 본문 길이 */} | ||
<section className={styles.sectionStyle}> | ||
<Label>본문 길이</Label> | ||
<Controller | ||
name="length" | ||
control={control} | ||
render={({ field: { onChange, value } }) => ( | ||
<RadioCards value={value} onChange={onChange} columns={3}> | ||
{LENGTH_OPTIONS.map( | ||
({ value, label, description, badge }) => ( | ||
<RadioCards.Item key={value} value={value}> | ||
<RadioCards.Badge>{badge}</RadioCards.Badge> | ||
<RadioCards.Label>{label}</RadioCards.Label> | ||
<RadioCards.Description> | ||
{description} | ||
</RadioCards.Description> | ||
</RadioCards.Item> | ||
) | ||
)} | ||
</RadioCards> | ||
)} | ||
/> | ||
</section> | ||
|
||
{/* 핵심 내용 */} | ||
<section className={styles.sectionStyle}> | ||
<TextField id="content"> | ||
<TextField.Label variant="optional">핵심 내용</TextField.Label> | ||
<Controller | ||
name="content" | ||
control={control} | ||
render={({ field }) => ( | ||
<TextField.Input | ||
{...field} | ||
placeholder="본문에 꼭 포함되어야 하는 문구나 요구 사항을 적어주세요" | ||
maxLength={5000} | ||
/> | ||
)} | ||
/> | ||
</TextField> | ||
</section> | ||
</form> | ||
</AnimatedContainer> | ||
</div> | ||
); | ||
} |
12 changes: 12 additions & 0 deletions
12
apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { vars } from '@repo/theme'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const containerStyle = style({ | ||
maxWidth: '92rem', | ||
margin: '0 auto', | ||
width: '100%', | ||
padding: `${vars.space[32]} ${vars.space[32]} 12rem ${vars.space[32]}`, | ||
|
||
borderRadius: '2.4rem 2.4rem 0 0', | ||
backgroundColor: vars.colors.grey, | ||
}); |
24 changes: 24 additions & 0 deletions
24
apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { motion } from 'motion/react'; | ||
import { ReactNode } from 'react'; | ||
import * as styles from './AnimatedContainer.css'; | ||
|
||
type AnimatedContainerProps = { | ||
children: ReactNode; | ||
}; | ||
|
||
export function AnimatedContainer({ children }: AnimatedContainerProps) { | ||
return ( | ||
<motion.div | ||
className={styles.containerStyle} | ||
initial={{ y: '100%' }} | ||
animate={{ y: 0 }} | ||
transition={{ | ||
type: 'spring', | ||
duration: 0.6, | ||
bounce: 0.22, | ||
}} | ||
> | ||
{children} | ||
</motion.div> | ||
); | ||
} |
24 changes: 24 additions & 0 deletions
24
apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { vars } from '@repo/theme'; | ||
import { keyframes, style } from '@vanilla-extract/css'; | ||
|
||
const flowingGradient = keyframes({ | ||
'0%': { | ||
backgroundPosition: '0% 50%', | ||
}, | ||
'100%': { | ||
backgroundPosition: '200% 50%', | ||
}, | ||
}); | ||
|
||
export const gradientTitleStyle = style({ | ||
background: | ||
'linear-gradient(90deg, #1F3761 0%, #2646C5 10%, #615BE7 30%, #615BE7 70%, #2646C5 93%, #1F3761 97%, #1F3761 100%)', | ||
backgroundSize: '200% 100%', | ||
backgroundClip: 'text', | ||
WebkitBackgroundClip: 'text', | ||
WebkitTextFillColor: 'transparent', | ||
textAlign: 'center', | ||
padding: `${vars.space[24]} 0`, | ||
animation: `${flowingGradient} 4s linear infinite`, | ||
backgroundRepeat: 'repeat', | ||
}); |
38 changes: 38 additions & 0 deletions
38
apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { motion } from 'motion/react'; | ||
import * as styles from './GradientAnimatedTitle.css'; | ||
import { ReactNode } from 'react'; | ||
|
||
type GradientAnimatedTitleProps = { | ||
children: ReactNode; | ||
}; | ||
|
||
export function GradientAnimatedTitle({ | ||
children, | ||
}: GradientAnimatedTitleProps) { | ||
return ( | ||
<motion.h1 | ||
className={styles.gradientTitleStyle} | ||
initial={{ | ||
y: '35vh', | ||
scale: 2, | ||
x: '-50%', | ||
left: '50%', | ||
position: 'absolute', | ||
}} | ||
animate={{ | ||
y: 0, | ||
scale: 1, | ||
x: 0, | ||
left: 'auto', | ||
position: 'relative', | ||
}} | ||
transition={{ | ||
type: 'spring', | ||
duration: 0.6, | ||
bounce: 0.22, | ||
}} | ||
> | ||
{children} | ||
</motion.h1> | ||
); | ||
} |
37 changes: 37 additions & 0 deletions
37
apps/web/src/app/create/_components/KeywordChip/KeywordChip.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { vars } from '@repo/theme'; | ||
import { style } from '@vanilla-extract/css'; | ||
import { recipe } from '@vanilla-extract/recipes'; | ||
|
||
export const KeywordChipRecipe = recipe({ | ||
base: { | ||
display: 'inline-flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
whiteSpace: 'nowrap', | ||
padding: '0 1.6rem', | ||
height: '4rem', | ||
width: 'fit-content', | ||
borderRadius: '2.4rem', | ||
backgroundColor: vars.colors.grey25, | ||
color: vars.colors.grey500, | ||
cursor: 'pointer', | ||
transition: 'all 0.2s ease', | ||
border: 'none', | ||
':hover': { | ||
opacity: 0.8, | ||
}, | ||
}, | ||
variants: { | ||
isSelected: { | ||
true: { | ||
backgroundColor: vars.colors.primary200, | ||
color: vars.colors.primary800, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
export const keywordChipGroupWrapper = style({ | ||
display: 'flex', | ||
gap: vars.space[10], | ||
}); |
Oops, something went wrong.