From 6a76712045068f2effd2d31a196edc4413b28a97 Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 05:38:21 +0900 Subject: [PATCH 01/21] =?UTF-8?q?chore(apps/web):=20motion=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/apps/web/package.json b/apps/web/package.json index 95dd905f..dc757068 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "@repo/theme": "workspace:^", "@repo/ui": "workspace:^", "@vanilla-extract/css": "^1.17.0", + "motion": "^11.17.0", "next": "14.2.21", "overlay-kit": "^1.4.1", "react": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 888b8899..0a5ee69a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: '@vanilla-extract/css': specifier: ^1.17.0 version: 1.17.0 + motion: + specifier: ^11.17.0 + version: 11.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: 14.2.21 version: 14.2.21(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) From 19acc4dba019d3259eefe25940e2fda488acca4c Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 05:38:52 +0900 Subject: [PATCH 02/21] =?UTF-8?q?chore(packages/ui):=20styles=20export=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 6b007dbb..d182c43a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,9 +18,7 @@ "types": "./dist/hooks/index.d.ts", "import": "./dist/hooks/index.js" }, - "./styles": { - "import": "./dist/index.css" - }, + "./styles": "./dist/index.css", "./IconButton": { "types": "./dist/components/IconButton/index.d.ts", "import": "./dist/components/IconButton/index.js" From 6d8c4b089c465e7bdf0eb0106497a15e177a648e Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 05:39:17 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat(packages/theme):=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/theme/src/themes/contract.ts | 1 + packages/theme/src/themes/dark.ts | 1 + packages/theme/src/themes/light.ts | 1 + packages/theme/src/tokens/colors.ts | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/theme/src/themes/contract.ts b/packages/theme/src/themes/contract.ts index 881e2a4d..ff06cc3f 100644 --- a/packages/theme/src/themes/contract.ts +++ b/packages/theme/src/themes/contract.ts @@ -32,6 +32,7 @@ export type ThemeContract = { grey950toPrimary: string; grey1000to1000: string; + primary200: string; primary500: string; primary600: string; primary700: string; diff --git a/packages/theme/src/themes/dark.ts b/packages/theme/src/themes/dark.ts index 4ae34faa..27405c88 100644 --- a/packages/theme/src/themes/dark.ts +++ b/packages/theme/src/themes/dark.ts @@ -36,6 +36,7 @@ export const darkTheme: ThemeContract = { grey950toPrimary: tokens.colors.green200, grey1000to1000: tokens.colors.grey1000, + primary200: tokens.colors.primary200, primary500: tokens.colors.primary500, primary600: tokens.colors.primary600, primary700: tokens.colors.primary700, diff --git a/packages/theme/src/themes/light.ts b/packages/theme/src/themes/light.ts index d5d35a04..d2c33ec7 100644 --- a/packages/theme/src/themes/light.ts +++ b/packages/theme/src/themes/light.ts @@ -36,6 +36,7 @@ export const lightTheme: ThemeContract = { grey950toPrimary: tokens.colors.grey950, grey1000to1000: tokens.colors.grey1000, + primary200: tokens.colors.primary200, primary500: tokens.colors.primary500, primary600: tokens.colors.primary600, primary700: tokens.colors.primary700, diff --git a/packages/theme/src/tokens/colors.ts b/packages/theme/src/tokens/colors.ts index ae2ca1c4..c095d131 100644 --- a/packages/theme/src/tokens/colors.ts +++ b/packages/theme/src/tokens/colors.ts @@ -1,8 +1,9 @@ export const colors = { + primary200: '#D8DDFF', primary500: '#7081F4', primary600: '#5266EC', primary700: '#3348D6', - primary800: '#2035BC', // TODO 삭제 예정 + primary800: '#2739B2', warning300: '#FF724E', warning500: '#FF3300', From f980e5ff7f1d0de2ddb56fa35f32d54c29aad02f Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 05:39:32 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat(packages/theme):=20spacing=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 --- packages/theme/src/tokens/spacing.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/theme/src/tokens/spacing.ts b/packages/theme/src/tokens/spacing.ts index 44450332..5394b507 100644 --- a/packages/theme/src/tokens/spacing.ts +++ b/packages/theme/src/tokens/spacing.ts @@ -9,6 +9,7 @@ export const spacing = { 32: '3.2rem', 40: '4.0rem', 64: '6.4rem', + 80: '8.0rem', 128: '12.8rem', } as const; From 40d4c21d82c7b9a6d0d9c36c9b6e139ecad945dd Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 05:39:58 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat(apps/web):=20KeywordChip=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KeywordChip/KeywordChip.css.ts | 37 ++++++++ .../_components/KeywordChip/KeywordChip.tsx | 74 +++++++++++++++ .../KeywordChip/KeywordChipGroup.tsx | 91 +++++++++++++++++++ .../_components/KeywordChip/context.tsx | 30 ++++++ 4 files changed, 232 insertions(+) create mode 100644 apps/web/src/app/create/_components/KeywordChip/KeywordChip.css.ts create mode 100644 apps/web/src/app/create/_components/KeywordChip/KeywordChip.tsx create mode 100644 apps/web/src/app/create/_components/KeywordChip/KeywordChipGroup.tsx create mode 100644 apps/web/src/app/create/_components/KeywordChip/context.tsx diff --git a/apps/web/src/app/create/_components/KeywordChip/KeywordChip.css.ts b/apps/web/src/app/create/_components/KeywordChip/KeywordChip.css.ts new file mode 100644 index 00000000..37cd7bc0 --- /dev/null +++ b/apps/web/src/app/create/_components/KeywordChip/KeywordChip.css.ts @@ -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], +}); diff --git a/apps/web/src/app/create/_components/KeywordChip/KeywordChip.tsx b/apps/web/src/app/create/_components/KeywordChip/KeywordChip.tsx new file mode 100644 index 00000000..0b685669 --- /dev/null +++ b/apps/web/src/app/create/_components/KeywordChip/KeywordChip.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { + ComponentPropsWithoutRef, + forwardRef, + useCallback, + useEffect, + useRef, +} from 'react'; +import { KeywordChipRecipe } from './KeywordChip.css'; +import { Text } from '@repo/ui'; +import { useKeywordChip } from './context'; +import { mergeRefs } from '@repo/ui/utils'; + +export type KeywordChipProps = ComponentPropsWithoutRef<'button'> & { + value: string; + disabled?: boolean; +}; + +export const KeywordChip = forwardRef( + ({ className = '', children, value, disabled, ...rest }, ref) => { + const { + onChange, + disabled: groupDisabled, + isSelected, + itemsRef, + onKeyDown, + } = useKeywordChip(); + const isDisabled = disabled || groupDisabled; + const selected = isSelected?.(value) ?? false; + const itemRef = useRef(null); + + useEffect(() => { + const currentRef = itemRef.current; + if (currentRef) { + itemsRef.push(currentRef); + return () => { + const index = itemsRef.indexOf(currentRef); + if (index !== -1) itemsRef.splice(index, 1); + }; + } + }, [itemsRef]); + + const handleClick = useCallback(() => { + if (!isDisabled) { + onChange?.(value); + } + }, [isDisabled, onChange, value]); + + return ( + + ); + } +); + +KeywordChip.displayName = 'KeywordChip'; diff --git a/apps/web/src/app/create/_components/KeywordChip/KeywordChipGroup.tsx b/apps/web/src/app/create/_components/KeywordChip/KeywordChipGroup.tsx new file mode 100644 index 00000000..458ef547 --- /dev/null +++ b/apps/web/src/app/create/_components/KeywordChip/KeywordChipGroup.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { KeyboardEvent, ReactNode, useCallback, useState } from 'react'; +import { KeywordChip } from './KeywordChip'; +import { keywordChipGroupWrapper } from './KeywordChip.css'; +import { KeywordChipProvider } from './context'; + +const KEYBOARD_KEY = { + ARROW_RIGHT: 'ArrowRight', + ARROW_LEFT: 'ArrowLeft', +} as const; + +type KeywordChipGroupProps = { + children: ReactNode[]; + onChange?: (value: string) => void; + defaultValue?: string; + disabled?: boolean; + name?: string; +}; + +export const KeywordChipGroup = ({ + children, + onChange, + defaultValue = '', + disabled = false, + name = 'keyword-group', +}: KeywordChipGroupProps) => { + const [value, setValue] = useState(defaultValue); + const [itemsRef] = useState([]); + + const handleChange = useCallback( + (newValue: string) => { + setValue(newValue); + onChange?.(newValue); + }, + [onChange] + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent, currentIndex: number) => { + const items = itemsRef; + let nextIndex: number; + + switch (event.key) { + case KEYBOARD_KEY.ARROW_RIGHT: + event.preventDefault(); + nextIndex = (currentIndex + 1) % items.length; + break; + case KEYBOARD_KEY.ARROW_LEFT: + event.preventDefault(); + nextIndex = (currentIndex - 1 + items.length) % items.length; + break; + default: + return; + } + + items[nextIndex]?.focus(); + }, + [itemsRef] + ); + + const isSelected = useCallback( + (itemValue: string) => itemValue === value, + [value] + ); + + return ( + +
+ {children.map((child) => ( + + {child} + + ))} +
+
+ ); +}; diff --git a/apps/web/src/app/create/_components/KeywordChip/context.tsx b/apps/web/src/app/create/_components/KeywordChip/context.tsx new file mode 100644 index 00000000..82366d35 --- /dev/null +++ b/apps/web/src/app/create/_components/KeywordChip/context.tsx @@ -0,0 +1,30 @@ +import { createContext, useContext, KeyboardEvent } from 'react'; + +type KeywordChipContextValue = { + value?: string; + onChange?: (value: string) => void; + disabled?: boolean; + isSelected?: (value: string) => boolean; + itemValue?: string; + itemsRef: HTMLButtonElement[]; + onKeyDown?: ( + event: KeyboardEvent, + currentIndex: number + ) => void; +}; + +const KeywordChipContext = createContext( + undefined +); + +export const useKeywordChip = () => { + const context = useContext(KeywordChipContext); + if (!context) { + throw new Error( + 'KeywordChip은 KeywordChipProvider 내부에서만 사용할 수 있습니다.' + ); + } + return context; +}; + +export const KeywordChipProvider = KeywordChipContext.Provider; From eee1c1c8656989e50a8344a38a0f296c43e8a8e0 Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 07:41:24 +0900 Subject: [PATCH 06/21] =?UTF-8?q?feat(apps/web):=20ImageManager=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageManager/ImageManager.css.ts | 25 ++++ .../_components/ImageManager/ImageManager.tsx | 121 ++++++++++++++++++ .../ImageManager/ImageUploader.css.ts | 21 +++ .../ImageManager/ImageUploader.tsx | 47 +++++++ .../ImageManager/UploadedImages.css.ts | 33 +++++ .../ImageManager/UploadedImages.tsx | 46 +++++++ .../_components/ImageManager/context.tsx | 22 ++++ .../create/_components/ImageManager/types.ts | 5 + 8 files changed, 320 insertions(+) create mode 100644 apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts create mode 100644 apps/web/src/app/create/_components/ImageManager/ImageManager.tsx create mode 100644 apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts create mode 100644 apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx create mode 100644 apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts create mode 100644 apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx create mode 100644 apps/web/src/app/create/_components/ImageManager/context.tsx create mode 100644 apps/web/src/app/create/_components/ImageManager/types.ts diff --git a/apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts b/apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts new file mode 100644 index 00000000..da6a1737 --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts @@ -0,0 +1,25 @@ +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; +import { vars } from '@repo/theme'; + +export const textContent = recipe({ + base: { + position: 'relative', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + gap: vars.space[8], + color: vars.colors.grey600, + }, + variants: { + isCenter: { + true: { alignItems: 'center' }, + false: { alignItems: 'flex-start' }, + }, + }, +}); + +export const imagesContent = style({ + display: 'flex', + justifyContent: 'flex-end', +}); diff --git a/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx b/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx new file mode 100644 index 00000000..f0a80293 --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { Icon, Text } from '@repo/ui'; +import { ImageManagerProvider } from './context'; +import { ImageUploader } from './ImageUploader'; +import { UploadedImages } from './UploadedImages'; +import * as styles from './ImageManager.css'; +import { useState, useCallback, useEffect } from 'react'; +import type { ImageFile } from './types'; +import { useToast } from '@repo/ui/hooks'; + +type ImageManagerProps = { + /** + * 이미지 파일 크기 제한 (MB) + * @default 10 + */ + maxFileSize?: number; + /** + * 이미지 파일 최대 개수 + * @default 5 + */ + maxFiles?: number; +}; + +export const ImageManager = ({ + maxFileSize = 10, + maxFiles = 5, +}: ImageManagerProps) => { + const [images, setImages] = useState([]); + const isImageUploaded = images.length > 0; + const toast = useToast(); + const handleUpload = useCallback( + (files: FileList) => { + // 파일 크기 체크 + const oversizedFiles = Array.from(files).filter( + (file) => file.size > maxFileSize * 1024 * 1024 + ); + + if (oversizedFiles.length > 0) { + toast.error(`파일 크기는 ${maxFileSize}MB 이하여야 합니다.`, 3000); + return; + } + + // 최대 파일 개수 체크 + if (images.length + files.length > maxFiles) { + toast.error( + `이미지는 최대 ${maxFiles}장까지 업로드할 수 있습니다.`, + 3000 + ); + return; + } + + // 이미지 파일 타입 체크 + const invalidFiles = Array.from(files).filter( + (file) => !file.type.startsWith('image/') + ); + + if (invalidFiles.length > 0) { + toast.error('이미지 파일만 업로드할 수 있습니다.', 3000); + return; + } + + const newFiles = Array.from(files).map((file) => ({ + id: crypto.randomUUID(), + file, + preview: URL.createObjectURL(file), + })); + + setImages((prev) => [...prev, ...newFiles]); + }, + [images.length, maxFiles, maxFileSize, toast] + ); + + const handleRemove = useCallback((id: string) => { + setImages((prevImages) => { + const targetImage = prevImages.find((image) => image.id === id); + if (targetImage) { + URL.revokeObjectURL(targetImage.preview); + } + return prevImages.filter((image) => image.id !== id); + }); + }, []); + + // 컴포넌트 언마운트 시 메모리 정리 + useEffect(() => { + return () => { + images.forEach((image) => { + URL.revokeObjectURL(image.preview); + }); + }; + }, [images]); + + return ( + + +
+ + + 이곳에 이미지를 드래그하거나 클릭하여 업로드 + + {!isImageUploaded && ( + + 최대 {maxFiles}장, 각 {maxFileSize}MB이하 + + )} +
+ {isImageUploaded && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts b/apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts new file mode 100644 index 00000000..c8d1936a --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts @@ -0,0 +1,21 @@ +import { vars } from '@repo/theme'; +import { style } from '@vanilla-extract/css'; + +export const uploader = style({ + position: 'relative', + width: '100%', + height: '9.6rem', + cursor: 'pointer', + backgroundColor: vars.colors.grey25, + borderRadius: vars.borderRadius[16], + padding: vars.space[16], + transition: 'all 0.2s ease', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + gap: vars.space[24], +}); + +export const input = style({ + display: 'none', +}); diff --git a/apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx b/apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx new file mode 100644 index 00000000..40a8b926 --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { ChangeEvent, DragEvent, ReactNode, useCallback } from 'react'; +import { useImageManager } from './context'; +import * as styles from './ImageUploader.css'; + +type ImageUploaderProps = { + children: ReactNode; +}; + +export const ImageUploader = ({ children }: ImageUploaderProps) => { + const { onUpload } = useImageManager(); + + const handleDrop = useCallback( + (e: DragEvent) => { + e.preventDefault(); + onUpload(e.dataTransfer.files); + }, + [onUpload] + ); + + const handleChange = useCallback( + (e: ChangeEvent) => { + if (e.target.files) { + onUpload(e.target.files); + } + }, + [onUpload] + ); + + return ( + + ); +}; diff --git a/apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts b/apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts new file mode 100644 index 00000000..10ac3ab6 --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts @@ -0,0 +1,33 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '@repo/theme'; + +export const container = style({ + display: 'flex', + gap: vars.space[12], + width: '100%', +}); + +export const imageWrapper = style({ + position: 'relative', + width: '6.4rem', + height: '6.4rem', +}); + +export const image = style({ + width: '100%', + height: '100%', + objectFit: 'cover', + borderRadius: vars.borderRadius[12], + cursor: 'pointer', +}); + +export const removeButton = style({ + position: 'absolute', + top: '-1rem', + right: '-1rem', + width: '2.4rem', + height: '2.4rem', + backgroundColor: 'transparent', + border: 'transparent', + cursor: 'pointer', +}); diff --git a/apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx b/apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx new file mode 100644 index 00000000..438959f2 --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { Icon } from '@repo/ui'; +import * as styles from './UploadedImages.css'; +import Image from 'next/image'; +import type { ImageFile } from './types'; +import type { ImageManagerContextValue } from './context'; + +type UploadedImagesProps = { + images: ImageFile[]; +} & Pick; + +const IMAGE_SIZE = 64; + +export const UploadedImages = ({ images, onRemove }: UploadedImagesProps) => { + return ( +
+ {images.map((image) => ( +
e.preventDefault()} // 이미지 클릭 시 삭제 방지 + > + + +
+ ))} +
+ ); +}; diff --git a/apps/web/src/app/create/_components/ImageManager/context.tsx b/apps/web/src/app/create/_components/ImageManager/context.tsx new file mode 100644 index 00000000..a8a6e89b --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/context.tsx @@ -0,0 +1,22 @@ +import { createContext, useContext } from 'react'; +import type { ImageFile } from './types'; + +export type ImageManagerContextValue = { + images: ImageFile[]; + onUpload: (files: FileList) => void; + onRemove: (id: string) => void; +}; + +const ImageManagerContext = createContext( + null +); + +export const useImageManager = () => { + const context = useContext(ImageManagerContext); + if (!context) { + throw new Error('ImageManager 컴포넌트 내부에서만 사용할 수 있습니다.'); + } + return context; +}; + +export const ImageManagerProvider = ImageManagerContext.Provider; diff --git a/apps/web/src/app/create/_components/ImageManager/types.ts b/apps/web/src/app/create/_components/ImageManager/types.ts new file mode 100644 index 00000000..12082c1b --- /dev/null +++ b/apps/web/src/app/create/_components/ImageManager/types.ts @@ -0,0 +1,5 @@ +export type ImageFile = { + id: string; + file: File; + preview: string; +}; From 9a36f61d42bfd621d23e4ae139a8f180a0ac2e84 Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 07:42:15 +0900 Subject: [PATCH 07/21] =?UTF-8?q?feat(apps/web):=20=EC=A3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8D=BC?= =?UTF-8?q?=EB=B8=94=EB=A6=AC=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 220 +++++++++++++++++++++++ apps/web/src/app/create/page.tsx | 5 + apps/web/src/app/create/pageStyle.css.ts | 70 ++++++++ 3 files changed, 295 insertions(+) create mode 100644 apps/web/src/app/create/Create.tsx create mode 100644 apps/web/src/app/create/page.tsx create mode 100644 apps/web/src/app/create/pageStyle.css.ts diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx new file mode 100644 index 00000000..f77496e1 --- /dev/null +++ b/apps/web/src/app/create/Create.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { + Breadcrumb, + TextField, + Button, + Label, + Icon, + Spacing, + RadioCards, +} from '@repo/ui'; +import Link from 'next/link'; +import * as styles from './pageStyle.css'; +import { motion } from 'motion/react'; +import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; +import { useState } from 'react'; +import { ImageManager } from './_components/ImageManager/ImageManager'; + +export default function Create() { + const [generationType, setGenerationType] = useState('1'); + + return ( +
+ {/* 헤더 */} +
+ + + + + + + + +
+ + + 어떤 글을 생성할까요? + + + {/* 메인 컨텐츠 */} +
+
+ {/* 주제 */} + + 주제 + + +
+ + {/* 목적 */} +
+ + + } + > + 정보 제공 + + + } + > + 의견 표출 + + + } + > + 공감/유머 + + + } + > + 홍보/마케팅 + + +
+ + {/* 생성 방식 */} +
+ + setGenerationType(value)} + > + } + > + 입력된 주제로만 생성 + + 주제에 맞는 글을 간단히 생성 + + + + } + > + 최근 뉴스로 글 생성 + + 최근 소식/뉴스 기반 + + + + } + > + 이미지를 참고해 글 생성 + + 첨부한 이미지 기반 + + + +
+ + {/* 뉴스 카테고리 - 생성 방식이 '최근 뉴스로 글 생성'일 때만 표시 */} + {generationType === '2' && ( +
+ + + {['투자', '패션', '피트니스', '헬스케어']} + +
+ )} + + {/* 이미지 업로더 - 생성 방식이 '이미지를 참고해 글 생성'일 때만 표시 */} + {generationType === '3' && ( + + )} + + {/* 본문 길이 */} +
+ + + + 누구나 이용 가능 + 짧은 게시물 + + 약 1~2문장, 최대 140자 + + + + X 유료 구독 전용 + 보통 게시물 + + 약 3~4문장, 최대 300자 + + + + X 유료 구독 전용 + 긴 게시물 + + 약 7~8문장, 최대 1000자 + + + +
+ + {/* 핵심 내용 */} +
+ + 핵심 내용 + + +
+
+
+
+ ); +} diff --git a/apps/web/src/app/create/page.tsx b/apps/web/src/app/create/page.tsx new file mode 100644 index 00000000..2e46186e --- /dev/null +++ b/apps/web/src/app/create/page.tsx @@ -0,0 +1,5 @@ +import Create from './Create'; + +export default function CreatePage() { + return ; +} diff --git a/apps/web/src/app/create/pageStyle.css.ts b/apps/web/src/app/create/pageStyle.css.ts new file mode 100644 index 00000000..68e30e4f --- /dev/null +++ b/apps/web/src/app/create/pageStyle.css.ts @@ -0,0 +1,70 @@ +import { style, keyframes } from '@vanilla-extract/css'; +import { vars } from '@repo/theme'; + +export const mainStyle = style({ + maxWidth: '100%', + height: '100vh', + margin: '0 auto', + background: 'radial-gradient(circle at 50% 0%, #D7DAFF 0%, #FFFFFF 76%)', + overflow: 'auto', +}); + +export const containerStyle = style({ + maxWidth: '92rem', + margin: '0 auto', + width: '100%', + minHeight: 'calc(100% + 12rem)', + padding: vars.space[32], + borderRadius: '2.4rem 2.4rem 0 0', + backgroundColor: vars.colors.grey, +}); + +export const headerStyle = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + position: 'fixed', + top: 0, + left: 0, + right: 0, + padding: `${vars.space[12]} ${vars.space[24]}`, +}); + +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', +}); + +export const contentStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: '4.8rem', +}); + +export const sectionStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: vars.space[16], +}); + +export const labelDiscriptionWrapperStyle = style({ + display: 'flex', + flexDirection: 'column', +}); From 50d14a6a8d569278baf039b6bebf18488d7b564c Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 08:00:08 +0900 Subject: [PATCH 08/21] =?UTF-8?q?refactor(apps/web):=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=9E=90?= =?UTF-8?q?=EC=9E=98=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 320 +++++++----------- .../AnimatedContainer.css.ts | 15 + .../AnimatedContainer/AnimatedContainer.tsx | 24 ++ .../AnimatedTitle/AnimatedTitle.css.ts | 24 ++ .../AnimatedTitle/AnimatedTitle.tsx | 31 ++ .../create/_components/Header/Header.css.ts | 14 + .../app/create/_components/Header/Header.tsx | 24 ++ .../_components/ImageManager/ImageManager.tsx | 6 +- apps/web/src/app/create/pageStyle.css.ts | 51 +-- .../components/TextField/TextFieldInput.tsx | 2 +- 10 files changed, 265 insertions(+), 246 deletions(-) create mode 100644 apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts create mode 100644 apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.tsx create mode 100644 apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.css.ts create mode 100644 apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx create mode 100644 apps/web/src/app/create/_components/Header/Header.css.ts create mode 100644 apps/web/src/app/create/_components/Header/Header.tsx diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index f77496e1..71bbd853 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -1,220 +1,156 @@ 'use client'; -import { - Breadcrumb, - TextField, - Button, - Label, - Icon, - Spacing, - RadioCards, -} from '@repo/ui'; -import Link from 'next/link'; +import { TextField, Label, Spacing, RadioCards } from '@repo/ui'; import * as styles from './pageStyle.css'; -import { motion } from 'motion/react'; import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; import { useState } from 'react'; import { ImageManager } from './_components/ImageManager/ImageManager'; +import { Header } from './_components/Header/Header'; +import { AnimatedTitle } from './_components/AnimatedTitle/AnimatedTitle'; +import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; export default function Create() { const [generationType, setGenerationType] = useState('1'); return (
- {/* 헤더 */} -
- - - - - - - - -
+
- - 어떤 글을 생성할까요? - - - {/* 메인 컨텐츠 */} -
-
- {/* 주제 */} - - 주제 - - -
+ + +
+ {/* 주제 */} + + 주제 + + +
- {/* 목적 */} -
- - - } - > - 정보 제공 - + {/* 목적 */} +
+ + + } + > + 정보 제공 + - } - > - 의견 표출 - + } + > + 의견 표출 + - } - > - 공감/유머 - + } + > + 공감/유머 + - } - > - 홍보/마케팅 - - -
+ } + > + 홍보/마케팅 + + + - {/* 생성 방식 */} -
- - setGenerationType(value)} + {/* 생성 방식 */} +
+ + setGenerationType(value)} + > + } > - } - > - 입력된 주제로만 생성 - - 주제에 맞는 글을 간단히 생성 - - + 입력된 주제로만 생성 + + 주제에 맞는 글을 간단히 생성 + + - } - > - 최근 뉴스로 글 생성 - - 최근 소식/뉴스 기반 - - + } + > + 최근 뉴스로 글 생성 + + 최근 소식/뉴스 기반 + + - } - > - 이미지를 참고해 글 생성 - - 첨부한 이미지 기반 - - - -
+ } + > + 이미지를 참고해 글 생성 + + 첨부한 이미지 기반 + + + + - {/* 뉴스 카테고리 - 생성 방식이 '최근 뉴스로 글 생성'일 때만 표시 */} - {generationType === '2' && ( -
- - - {['투자', '패션', '피트니스', '헬스케어']} - -
- )} + {/* 뉴스 카테고리 - 생성 방식이 '최근 뉴스로 글 생성'일 때만 표시 */} + {generationType === '2' && ( +
+ + + {['투자', '패션', '피트니스', '헬스케어']} + +
+ )} - {/* 이미지 업로더 - 생성 방식이 '이미지를 참고해 글 생성'일 때만 표시 */} - {generationType === '3' && ( - - )} + {/* 이미지 업로더 - 생성 방식이 '이미지를 참고해 글 생성'일 때만 표시 */} + {generationType === '3' && ( + + )} - {/* 본문 길이 */} -
- - - - 누구나 이용 가능 - 짧은 게시물 - - 약 1~2문장, 최대 140자 - - - - X 유료 구독 전용 - 보통 게시물 - - 약 3~4문장, 최대 300자 - - - - X 유료 구독 전용 - 긴 게시물 - - 약 7~8문장, 최대 1000자 - - - -
+ {/* 본문 길이 */} +
+ + + + 누구나 이용 가능 + 짧은 게시물 + + 약 1~2문장, 최대 140자 + + + + X 유료 구독 전용 + 보통 게시물 + + 약 3~4문장, 최대 300자 + + + + X 유료 구독 전용 + 긴 게시물 + + 약 7~8문장, 최대 1000자 + + + +
- {/* 핵심 내용 */} -
- - 핵심 내용 - - -
-
-
+ {/* 핵심 내용 */} +
+ + 핵심 내용 + + +
+
); } diff --git a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts new file mode 100644 index 00000000..1e5b5375 --- /dev/null +++ b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts @@ -0,0 +1,15 @@ +import { vars } from '@repo/theme'; +import { style } from '@vanilla-extract/css'; + +export const containerStyle = style({ + maxWidth: '92rem', + margin: '0 auto', + width: '100%', + minHeight: 'calc(100% + 12rem)', + padding: vars.space[32], + borderRadius: '2.4rem 2.4rem 0 0', + backgroundColor: vars.colors.grey, + display: 'flex', + flexDirection: 'column', + gap: '4.8rem', +}); diff --git a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.tsx b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.tsx new file mode 100644 index 00000000..e850e95e --- /dev/null +++ b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.tsx @@ -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 ( + + {children} + + ); +} diff --git a/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.css.ts b/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.css.ts new file mode 100644 index 00000000..52db0369 --- /dev/null +++ b/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.css.ts @@ -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', +}); diff --git a/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx b/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx new file mode 100644 index 00000000..a9ead655 --- /dev/null +++ b/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx @@ -0,0 +1,31 @@ +import { motion } from 'motion/react'; +import * as styles from './AnimatedTitle.css'; + +export function AnimatedTitle() { + return ( + + 어떤 글을 생성할까요? + + ); +} diff --git a/apps/web/src/app/create/_components/Header/Header.css.ts b/apps/web/src/app/create/_components/Header/Header.css.ts new file mode 100644 index 00000000..2f0b55f1 --- /dev/null +++ b/apps/web/src/app/create/_components/Header/Header.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '@repo/theme'; + +export const headerStyle = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + position: 'fixed', + top: 0, + left: 0, + right: 0, + padding: `${vars.space[12]} ${vars.space[24]}`, + zIndex: 1000, +}); diff --git a/apps/web/src/app/create/_components/Header/Header.tsx b/apps/web/src/app/create/_components/Header/Header.tsx new file mode 100644 index 00000000..ef61fe93 --- /dev/null +++ b/apps/web/src/app/create/_components/Header/Header.tsx @@ -0,0 +1,24 @@ +import { Breadcrumb, Button, Icon } from '@repo/ui'; +import Link from 'next/link'; +import * as styles from './Header.css'; + +export function Header() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx b/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx index f0a80293..65021306 100644 --- a/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx +++ b/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx @@ -37,14 +37,14 @@ export const ImageManager = ({ ); if (oversizedFiles.length > 0) { - toast.error(`파일 크기는 ${maxFileSize}MB 이하여야 합니다.`, 3000); + toast.error(`파일 크기는 ${maxFileSize}MB 이하여야 해요.`, 3000); return; } // 최대 파일 개수 체크 if (images.length + files.length > maxFiles) { toast.error( - `이미지는 최대 ${maxFiles}장까지 업로드할 수 있습니다.`, + `이미지는 최대 ${maxFiles}장까지 업로드할 수 있어요.`, 3000 ); return; @@ -56,7 +56,7 @@ export const ImageManager = ({ ); if (invalidFiles.length > 0) { - toast.error('이미지 파일만 업로드할 수 있습니다.', 3000); + toast.error('이미지 파일만 업로드할 수 있어요.', 3000); return; } diff --git a/apps/web/src/app/create/pageStyle.css.ts b/apps/web/src/app/create/pageStyle.css.ts index 68e30e4f..bdce7e39 100644 --- a/apps/web/src/app/create/pageStyle.css.ts +++ b/apps/web/src/app/create/pageStyle.css.ts @@ -1,4 +1,4 @@ -import { style, keyframes } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; import { vars } from '@repo/theme'; export const mainStyle = style({ @@ -9,55 +9,6 @@ export const mainStyle = style({ overflow: 'auto', }); -export const containerStyle = style({ - maxWidth: '92rem', - margin: '0 auto', - width: '100%', - minHeight: 'calc(100% + 12rem)', - padding: vars.space[32], - borderRadius: '2.4rem 2.4rem 0 0', - backgroundColor: vars.colors.grey, -}); - -export const headerStyle = style({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - position: 'fixed', - top: 0, - left: 0, - right: 0, - padding: `${vars.space[12]} ${vars.space[24]}`, -}); - -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', -}); - -export const contentStyle = style({ - display: 'flex', - flexDirection: 'column', - gap: '4.8rem', -}); - export const sectionStyle = style({ display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/components/TextField/TextFieldInput.tsx b/packages/ui/src/components/TextField/TextFieldInput.tsx index 82c0a839..74fc856c 100644 --- a/packages/ui/src/components/TextField/TextFieldInput.tsx +++ b/packages/ui/src/components/TextField/TextFieldInput.tsx @@ -29,7 +29,7 @@ export const TextFieldInput = forwardRef< ( { maxLength = 500, - showCounter = true, + showCounter = false, value: controlledValue, defaultValue, className = '', From 54783d03657901f59cfb0b6c236c3580785633cc Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 08:16:27 +0900 Subject: [PATCH 09/21] =?UTF-8?q?fix(apps/web):=20react-hook-form=20watch?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EC=A1=B0=EA=B1=B4=EB=B6=80=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 286 ++++++++++-------- .../AnimatedContainer.css.ts | 3 - apps/web/src/app/create/pageStyle.css.ts | 6 + 3 files changed, 161 insertions(+), 134 deletions(-) diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index 71bbd853..06868ad8 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -3,14 +3,24 @@ import { TextField, Label, Spacing, RadioCards } from '@repo/ui'; import * as styles from './pageStyle.css'; import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; -import { useState } from 'react'; import { ImageManager } from './_components/ImageManager/ImageManager'; import { Header } from './_components/Header/Header'; import { AnimatedTitle } from './_components/AnimatedTitle/AnimatedTitle'; import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; +import { useForm, Controller } from 'react-hook-form'; + +interface CreateFormValues { + reference: '1' | '2' | '3'; +} export default function Create() { - const [generationType, setGenerationType] = useState('1'); + const { watch, control } = useForm({ + defaultValues: { + reference: '1', + }, + }); + + const generationType = watch('reference'); return (
@@ -18,138 +28,152 @@ export default function Create() { -
+
{/* 주제 */} - - 주제 - - -
- - {/* 목적 */} -
- - - } - > - 정보 제공 - - - } - > - 의견 표출 - - - } - > - 공감/유머 - - - } - > - 홍보/마케팅 - - -
- - {/* 생성 방식 */} -
- - setGenerationType(value)} - > - } - > - 입력된 주제로만 생성 - - 주제에 맞는 글을 간단히 생성 - - - - } - > - 최근 뉴스로 글 생성 - - 최근 소식/뉴스 기반 - - - - } - > - 이미지를 참고해 글 생성 - - 첨부한 이미지 기반 - - - -
- - {/* 뉴스 카테고리 - 생성 방식이 '최근 뉴스로 글 생성'일 때만 표시 */} - {generationType === '2' && (
- - - {['투자', '패션', '피트니스', '헬스케어']} - + + 주제 + + +
+ + {/* 목적 */} +
+ + + } + > + 정보 제공 + + + } + > + 의견 표출 + + + } + > + 공감/유머 + + + } + > + 홍보/마케팅 + + +
+ + {/* 생성 방식 */} +
+ + ( + + } + > + 입력된 주제로만 생성 + + 주제에 맞는 글을 간단히 생성 + + + + } + > + 최근 뉴스로 글 생성 + + 최근 소식/뉴스 기반 + + + + } + > + 이미지를 참고해 글 생성 + + 첨부한 이미지 기반 + + + + )} + /> +
+ + {/* 조건부 렌더링 섹션들 */} + {generationType === '2' && ( +
+ + + {['투자', '패션', '피트니스', '헬스케어']} + +
+ )} + + {generationType === '3' && ( + + )} + + {/* 본문 길이 */} +
+ + + + 누구나 이용 가능 + 짧은 게시물 + + 약 1~2문장, 최대 140자 + + + + X 유료 구독 전용 + 보통 게시물 + + 약 3~4문장, 최대 300자 + + + + X 유료 구독 전용 + 긴 게시물 + + 약 7~8문장, 최대 1000자 + + + +
+ + {/* 핵심 내용 */} +
+ + 핵심 내용 + +
- )} - - {/* 이미지 업로더 - 생성 방식이 '이미지를 참고해 글 생성'일 때만 표시 */} - {generationType === '3' && ( - - )} - - {/* 본문 길이 */} -
- - - - 누구나 이용 가능 - 짧은 게시물 - - 약 1~2문장, 최대 140자 - - - - X 유료 구독 전용 - 보통 게시물 - - 약 3~4문장, 최대 300자 - - - - X 유료 구독 전용 - 긴 게시물 - - 약 7~8문장, 최대 1000자 - - - -
- - {/* 핵심 내용 */} -
- - 핵심 내용 - - -
+
); diff --git a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts index 1e5b5375..ff89b17c 100644 --- a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts +++ b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts @@ -9,7 +9,4 @@ export const containerStyle = style({ padding: vars.space[32], borderRadius: '2.4rem 2.4rem 0 0', backgroundColor: vars.colors.grey, - display: 'flex', - flexDirection: 'column', - gap: '4.8rem', }); diff --git a/apps/web/src/app/create/pageStyle.css.ts b/apps/web/src/app/create/pageStyle.css.ts index bdce7e39..3aa2507c 100644 --- a/apps/web/src/app/create/pageStyle.css.ts +++ b/apps/web/src/app/create/pageStyle.css.ts @@ -9,6 +9,12 @@ export const mainStyle = style({ overflow: 'auto', }); +export const contentStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: '4.8rem', +}); + export const sectionStyle = style({ display: 'flex', flexDirection: 'column', From c4710ef1fbb2f26a9b2509d0d6cf0f7e87f9aa67 Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 08:42:06 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat(apps/web):=20react-hook-form?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=9C=20=ED=8F=BC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 233 ++++++++++++------ .../create/_components/Header/Header.css.ts | 14 -- .../app/create/_components/Header/Header.tsx | 24 -- apps/web/src/app/create/pageStyle.css.ts | 12 + 4 files changed, 168 insertions(+), 115 deletions(-) delete mode 100644 apps/web/src/app/create/_components/Header/Header.css.ts delete mode 100644 apps/web/src/app/create/_components/Header/Header.tsx diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index 06868ad8..0d322f55 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -1,30 +1,82 @@ 'use client'; -import { TextField, Label, Spacing, RadioCards } from '@repo/ui'; +import { + TextField, + Label, + Spacing, + RadioCards, + Breadcrumb, + Icon, + Button, +} from '@repo/ui'; import * as styles from './pageStyle.css'; import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; import { ImageManager } from './_components/ImageManager/ImageManager'; -import { Header } from './_components/Header/Header'; import { AnimatedTitle } from './_components/AnimatedTitle/AnimatedTitle'; import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; import { useForm, Controller } from 'react-hook-form'; +import Link from 'next/link'; +import { isNil } from '@repo/ui/utils'; interface CreateFormValues { - reference: '1' | '2' | '3'; + topic: string; + purpose: 'INFORMATION' | 'OPINION' | 'HUMOR' | 'MARKETING'; + reference: 'NONE' | 'NEWS' | 'IMAGE'; + newsCategory?: string; + imageUrls?: string[]; + length: 'SHORT' | 'MEDIUM' | 'LONG'; + content: string; } export default function Create() { - const { watch, control } = useForm({ + const { watch, control, handleSubmit } = useForm({ defaultValues: { - reference: '1', + topic: '', + purpose: 'INFORMATION', + reference: 'NONE', + newsCategory: undefined, // TODO: 백엔드로부터 받는 데이터 타입으로 수정 + imageUrls: [], // TODO: presigned url 받아서 첨부 + length: 'SHORT', + content: '', }, + mode: 'onChange', }); - const generationType = watch('reference'); + const topic = watch('topic'); + const reference = watch('reference'); + + const onSubmit = (data: CreateFormValues) => { + const requestData = { + ...data, + newsCategory: data.reference === 'NEWS' ? data.newsCategory : null, + imageUrls: data.reference === 'IMAGE' ? data.imageUrls : null, + }; + + console.log('폼 데이터:', requestData); + }; + + const isSubmitDisabled = isEmptyStringOrNil(topic); return (
-
+
+ + + + + + + + +
@@ -33,9 +85,16 @@ export default function Create() {
주제 - ( + + )} />
@@ -43,35 +102,41 @@ export default function Create() { {/* 목적 */}
- - } - > - 정보 제공 - - - } - > - 의견 표출 - - - } - > - 공감/유머 - - - } - > - 홍보/마케팅 - - + ( + + } + > + 정보 제공 + + + } + > + 의견 표출 + + + } + > + 공감/유머 + + + } + > + 홍보/마케팅 + + + )} + />
{/* 생성 방식 */} @@ -81,14 +146,9 @@ export default function Create() { name="reference" control={control} render={({ field: { onChange, value } }) => ( - + } > 입력된 주제로만 생성 @@ -98,7 +158,7 @@ export default function Create() { } > 최근 뉴스로 글 생성 @@ -108,7 +168,7 @@ export default function Create() { } > 이미지를 참고해 글 생성 @@ -122,7 +182,7 @@ export default function Create() { {/* 조건부 렌더링 섹션들 */} - {generationType === '2' && ( + {reference === 'NEWS' && (
@@ -131,45 +191,58 @@ export default function Create() {
)} - {generationType === '3' && ( + {reference === 'IMAGE' && ( )} {/* 본문 길이 */}
- - - 누구나 이용 가능 - 짧은 게시물 - - 약 1~2문장, 최대 140자 - - - - X 유료 구독 전용 - 보통 게시물 - - 약 3~4문장, 최대 300자 - - - - X 유료 구독 전용 - 긴 게시물 - - 약 7~8문장, 최대 1000자 - - - + ( + + + 누구나 이용 가능 + 짧은 게시물 + + 약 1~2문장, 최대 140자 + + + + X 유료 구독 전용 + 보통 게시물 + + 약 3~4문장, 최대 300자 + + + + X 유료 구독 전용 + 긴 게시물 + + 약 7~8문장, 최대 1000자 + + + + )} + />
{/* 핵심 내용 */}
- + 핵심 내용 - ( + + )} />
@@ -178,3 +251,9 @@ export default function Create() {
); } + +type NullableString = string | null | undefined; + +function isEmptyStringOrNil(value: NullableString): boolean { + return isNil(value) || value.trim() === ''; +} diff --git a/apps/web/src/app/create/_components/Header/Header.css.ts b/apps/web/src/app/create/_components/Header/Header.css.ts deleted file mode 100644 index 2f0b55f1..00000000 --- a/apps/web/src/app/create/_components/Header/Header.css.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { vars } from '@repo/theme'; - -export const headerStyle = style({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - position: 'fixed', - top: 0, - left: 0, - right: 0, - padding: `${vars.space[12]} ${vars.space[24]}`, - zIndex: 1000, -}); diff --git a/apps/web/src/app/create/_components/Header/Header.tsx b/apps/web/src/app/create/_components/Header/Header.tsx deleted file mode 100644 index ef61fe93..00000000 --- a/apps/web/src/app/create/_components/Header/Header.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Breadcrumb, Button, Icon } from '@repo/ui'; -import Link from 'next/link'; -import * as styles from './Header.css'; - -export function Header() { - return ( -
- - - - - - - - -
- ); -} diff --git a/apps/web/src/app/create/pageStyle.css.ts b/apps/web/src/app/create/pageStyle.css.ts index 3aa2507c..559d197b 100644 --- a/apps/web/src/app/create/pageStyle.css.ts +++ b/apps/web/src/app/create/pageStyle.css.ts @@ -9,6 +9,18 @@ export const mainStyle = style({ overflow: 'auto', }); +export const headerStyle = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + position: 'fixed', + top: 0, + left: 0, + right: 0, + padding: `${vars.space[12]} ${vars.space[24]}`, + zIndex: 1000, +}); + export const contentStyle = style({ display: 'flex', flexDirection: 'column', From d20667a6421b8cf1fa63500e50036b8a161c80dc Mon Sep 17 00:00:00 2001 From: minseong Date: Thu, 30 Jan 2025 18:03:10 +0900 Subject: [PATCH 11/21] =?UTF-8?q?fix(apps/web):=20ImageManager=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20packages/ui=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 5 +- .../ImageManager/ImageManager.css.ts | 25 ---- .../_components/ImageManager/ImageManager.tsx | 121 ------------------ .../ImageManager/ImageUploader.css.ts | 21 --- .../ImageManager/ImageUploader.tsx | 47 ------- .../ImageManager/UploadedImages.css.ts | 33 ----- .../ImageManager/UploadedImages.tsx | 46 ------- .../_components/ImageManager/context.tsx | 22 ---- .../create/_components/ImageManager/types.ts | 5 - 9 files changed, 1 insertion(+), 324 deletions(-) delete mode 100644 apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts delete mode 100644 apps/web/src/app/create/_components/ImageManager/ImageManager.tsx delete mode 100644 apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts delete mode 100644 apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx delete mode 100644 apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts delete mode 100644 apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx delete mode 100644 apps/web/src/app/create/_components/ImageManager/context.tsx delete mode 100644 apps/web/src/app/create/_components/ImageManager/types.ts diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index 0d322f55..bb771908 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -11,7 +11,6 @@ import { } from '@repo/ui'; import * as styles from './pageStyle.css'; import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; -import { ImageManager } from './_components/ImageManager/ImageManager'; import { AnimatedTitle } from './_components/AnimatedTitle/AnimatedTitle'; import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; import { useForm, Controller } from 'react-hook-form'; @@ -191,9 +190,7 @@ export default function Create() { )} - {reference === 'IMAGE' && ( - - )} + {reference === 'IMAGE' && <>ImageManager} {/* 본문 길이 */}
diff --git a/apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts b/apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts deleted file mode 100644 index da6a1737..00000000 --- a/apps/web/src/app/create/_components/ImageManager/ImageManager.css.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { recipe } from '@vanilla-extract/recipes'; -import { vars } from '@repo/theme'; - -export const textContent = recipe({ - base: { - position: 'relative', - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - gap: vars.space[8], - color: vars.colors.grey600, - }, - variants: { - isCenter: { - true: { alignItems: 'center' }, - false: { alignItems: 'flex-start' }, - }, - }, -}); - -export const imagesContent = style({ - display: 'flex', - justifyContent: 'flex-end', -}); diff --git a/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx b/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx deleted file mode 100644 index 65021306..00000000 --- a/apps/web/src/app/create/_components/ImageManager/ImageManager.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client'; - -import { Icon, Text } from '@repo/ui'; -import { ImageManagerProvider } from './context'; -import { ImageUploader } from './ImageUploader'; -import { UploadedImages } from './UploadedImages'; -import * as styles from './ImageManager.css'; -import { useState, useCallback, useEffect } from 'react'; -import type { ImageFile } from './types'; -import { useToast } from '@repo/ui/hooks'; - -type ImageManagerProps = { - /** - * 이미지 파일 크기 제한 (MB) - * @default 10 - */ - maxFileSize?: number; - /** - * 이미지 파일 최대 개수 - * @default 5 - */ - maxFiles?: number; -}; - -export const ImageManager = ({ - maxFileSize = 10, - maxFiles = 5, -}: ImageManagerProps) => { - const [images, setImages] = useState([]); - const isImageUploaded = images.length > 0; - const toast = useToast(); - const handleUpload = useCallback( - (files: FileList) => { - // 파일 크기 체크 - const oversizedFiles = Array.from(files).filter( - (file) => file.size > maxFileSize * 1024 * 1024 - ); - - if (oversizedFiles.length > 0) { - toast.error(`파일 크기는 ${maxFileSize}MB 이하여야 해요.`, 3000); - return; - } - - // 최대 파일 개수 체크 - if (images.length + files.length > maxFiles) { - toast.error( - `이미지는 최대 ${maxFiles}장까지 업로드할 수 있어요.`, - 3000 - ); - return; - } - - // 이미지 파일 타입 체크 - const invalidFiles = Array.from(files).filter( - (file) => !file.type.startsWith('image/') - ); - - if (invalidFiles.length > 0) { - toast.error('이미지 파일만 업로드할 수 있어요.', 3000); - return; - } - - const newFiles = Array.from(files).map((file) => ({ - id: crypto.randomUUID(), - file, - preview: URL.createObjectURL(file), - })); - - setImages((prev) => [...prev, ...newFiles]); - }, - [images.length, maxFiles, maxFileSize, toast] - ); - - const handleRemove = useCallback((id: string) => { - setImages((prevImages) => { - const targetImage = prevImages.find((image) => image.id === id); - if (targetImage) { - URL.revokeObjectURL(targetImage.preview); - } - return prevImages.filter((image) => image.id !== id); - }); - }, []); - - // 컴포넌트 언마운트 시 메모리 정리 - useEffect(() => { - return () => { - images.forEach((image) => { - URL.revokeObjectURL(image.preview); - }); - }; - }, [images]); - - return ( - - -
- - - 이곳에 이미지를 드래그하거나 클릭하여 업로드 - - {!isImageUploaded && ( - - 최대 {maxFiles}장, 각 {maxFileSize}MB이하 - - )} -
- {isImageUploaded && ( -
- -
- )} -
-
- ); -}; diff --git a/apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts b/apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts deleted file mode 100644 index c8d1936a..00000000 --- a/apps/web/src/app/create/_components/ImageManager/ImageUploader.css.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { vars } from '@repo/theme'; -import { style } from '@vanilla-extract/css'; - -export const uploader = style({ - position: 'relative', - width: '100%', - height: '9.6rem', - cursor: 'pointer', - backgroundColor: vars.colors.grey25, - borderRadius: vars.borderRadius[16], - padding: vars.space[16], - transition: 'all 0.2s ease', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - gap: vars.space[24], -}); - -export const input = style({ - display: 'none', -}); diff --git a/apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx b/apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx deleted file mode 100644 index 40a8b926..00000000 --- a/apps/web/src/app/create/_components/ImageManager/ImageUploader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { ChangeEvent, DragEvent, ReactNode, useCallback } from 'react'; -import { useImageManager } from './context'; -import * as styles from './ImageUploader.css'; - -type ImageUploaderProps = { - children: ReactNode; -}; - -export const ImageUploader = ({ children }: ImageUploaderProps) => { - const { onUpload } = useImageManager(); - - const handleDrop = useCallback( - (e: DragEvent) => { - e.preventDefault(); - onUpload(e.dataTransfer.files); - }, - [onUpload] - ); - - const handleChange = useCallback( - (e: ChangeEvent) => { - if (e.target.files) { - onUpload(e.target.files); - } - }, - [onUpload] - ); - - return ( - - ); -}; diff --git a/apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts b/apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts deleted file mode 100644 index 10ac3ab6..00000000 --- a/apps/web/src/app/create/_components/ImageManager/UploadedImages.css.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { vars } from '@repo/theme'; - -export const container = style({ - display: 'flex', - gap: vars.space[12], - width: '100%', -}); - -export const imageWrapper = style({ - position: 'relative', - width: '6.4rem', - height: '6.4rem', -}); - -export const image = style({ - width: '100%', - height: '100%', - objectFit: 'cover', - borderRadius: vars.borderRadius[12], - cursor: 'pointer', -}); - -export const removeButton = style({ - position: 'absolute', - top: '-1rem', - right: '-1rem', - width: '2.4rem', - height: '2.4rem', - backgroundColor: 'transparent', - border: 'transparent', - cursor: 'pointer', -}); diff --git a/apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx b/apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx deleted file mode 100644 index 438959f2..00000000 --- a/apps/web/src/app/create/_components/ImageManager/UploadedImages.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { Icon } from '@repo/ui'; -import * as styles from './UploadedImages.css'; -import Image from 'next/image'; -import type { ImageFile } from './types'; -import type { ImageManagerContextValue } from './context'; - -type UploadedImagesProps = { - images: ImageFile[]; -} & Pick; - -const IMAGE_SIZE = 64; - -export const UploadedImages = ({ images, onRemove }: UploadedImagesProps) => { - return ( -
- {images.map((image) => ( -
e.preventDefault()} // 이미지 클릭 시 삭제 방지 - > - - -
- ))} -
- ); -}; diff --git a/apps/web/src/app/create/_components/ImageManager/context.tsx b/apps/web/src/app/create/_components/ImageManager/context.tsx deleted file mode 100644 index a8a6e89b..00000000 --- a/apps/web/src/app/create/_components/ImageManager/context.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { createContext, useContext } from 'react'; -import type { ImageFile } from './types'; - -export type ImageManagerContextValue = { - images: ImageFile[]; - onUpload: (files: FileList) => void; - onRemove: (id: string) => void; -}; - -const ImageManagerContext = createContext( - null -); - -export const useImageManager = () => { - const context = useContext(ImageManagerContext); - if (!context) { - throw new Error('ImageManager 컴포넌트 내부에서만 사용할 수 있습니다.'); - } - return context; -}; - -export const ImageManagerProvider = ImageManagerContext.Provider; diff --git a/apps/web/src/app/create/_components/ImageManager/types.ts b/apps/web/src/app/create/_components/ImageManager/types.ts deleted file mode 100644 index 12082c1b..00000000 --- a/apps/web/src/app/create/_components/ImageManager/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ImageFile = { - id: string; - file: File; - preview: string; -}; From e9fa937cbcf4daea4690d04d74b922c4a74d9341 Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 03:23:31 +0900 Subject: [PATCH 12/21] =?UTF-8?q?fix(apps/web):=20ImageManager=20TypeA=20c?= =?UTF-8?q?ss=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/ImageManager/TypeA/TypeA.css.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/common/ImageManager/TypeA/TypeA.css.ts b/apps/web/src/components/common/ImageManager/TypeA/TypeA.css.ts index da6a1737..a7b50a74 100644 --- a/apps/web/src/components/common/ImageManager/TypeA/TypeA.css.ts +++ b/apps/web/src/components/common/ImageManager/TypeA/TypeA.css.ts @@ -10,11 +10,12 @@ export const textContent = recipe({ justifyContent: 'center', gap: vars.space[8], color: vars.colors.grey600, + width: '100%', }, variants: { isCenter: { - true: { alignItems: 'center' }, - false: { alignItems: 'flex-start' }, + true: { justifyContent: 'center' }, + false: { justifyContent: 'flex-start' }, }, }, }); From b2f1e7e10605587fbfa27c2aa724f590f93f2a0d Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 06:51:29 +0900 Subject: [PATCH 13/21] =?UTF-8?q?fix(apps/web):=20GradientAnimatedTitle?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GradientAnimatedTitle.css.ts} | 0 .../GradientAnimatedTitle.tsx} | 13 ++++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) rename apps/web/src/app/create/_components/{AnimatedTitle/AnimatedTitle.css.ts => GradientAnimatedTitle/GradientAnimatedTitle.css.ts} (100%) rename apps/web/src/app/create/_components/{AnimatedTitle/AnimatedTitle.tsx => GradientAnimatedTitle/GradientAnimatedTitle.tsx} (66%) diff --git a/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.css.ts b/apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.css.ts similarity index 100% rename from apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.css.ts rename to apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.css.ts diff --git a/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx b/apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.tsx similarity index 66% rename from apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx rename to apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.tsx index a9ead655..1444e3ab 100644 --- a/apps/web/src/app/create/_components/AnimatedTitle/AnimatedTitle.tsx +++ b/apps/web/src/app/create/_components/GradientAnimatedTitle/GradientAnimatedTitle.tsx @@ -1,7 +1,14 @@ import { motion } from 'motion/react'; -import * as styles from './AnimatedTitle.css'; +import * as styles from './GradientAnimatedTitle.css'; +import { ReactNode } from 'react'; -export function AnimatedTitle() { +type GradientAnimatedTitleProps = { + children: ReactNode; +}; + +export function GradientAnimatedTitle({ + children, +}: GradientAnimatedTitleProps) { return ( - 어떤 글을 생성할까요? + {children} ); } From ae685378b4c326497e67e61dff818fd51b73f2d1 Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 06:51:56 +0900 Subject: [PATCH 14/21] =?UTF-8?q?fix(apps/web):=20GradientAnimatedTitle?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index bb771908..41149c18 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -11,7 +11,7 @@ import { } from '@repo/ui'; import * as styles from './pageStyle.css'; import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; -import { AnimatedTitle } from './_components/AnimatedTitle/AnimatedTitle'; +import { GradientAnimatedTitle } from './_components/GradientAnimatedTitle/GradientAnimatedTitle'; import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; import { useForm, Controller } from 'react-hook-form'; import Link from 'next/link'; @@ -77,7 +77,7 @@ export default function Create() { - + 어떤 글을 생성할까요?
{/* 주제 */} @@ -155,7 +155,6 @@ export default function Create() { 주제에 맞는 글을 간단히 생성 - } @@ -165,7 +164,6 @@ export default function Create() { 최근 소식/뉴스 기반 - } From 25a65ee4ed7dfd97475692fd0429ef9d0df1a7c0 Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 07:28:54 +0900 Subject: [PATCH 15/21] =?UTF-8?q?feat(apps/web):=20isEmptyStringOrNil=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/utils/index.ts | 1 + apps/web/src/utils/isEmptyStringOrNil.ts | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 apps/web/src/utils/isEmptyStringOrNil.ts diff --git a/apps/web/src/utils/index.ts b/apps/web/src/utils/index.ts index 014ae385..71c2faf2 100644 --- a/apps/web/src/utils/index.ts +++ b/apps/web/src/utils/index.ts @@ -1 +1,2 @@ export { validateFiles } from './validateFiles'; +export { isEmptyStringOrNil } from './isEmptyStringOrNil'; diff --git a/apps/web/src/utils/isEmptyStringOrNil.ts b/apps/web/src/utils/isEmptyStringOrNil.ts new file mode 100644 index 00000000..9050c01c --- /dev/null +++ b/apps/web/src/utils/isEmptyStringOrNil.ts @@ -0,0 +1,7 @@ +import { isNil } from '@repo/ui/utils'; + +type NullableString = string | null | undefined; + +export function isEmptyStringOrNil(value: NullableString): boolean { + return isNil(value) || value.trim() === ''; +} From efd6a6b733ef9d132fb3a584758a1676409f83bb Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 07:29:39 +0900 Subject: [PATCH 16/21] =?UTF-8?q?fix(apps/web):=20ImageManager=20TypeA=20?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=ED=98=95=EC=9C=BC=EB=A1=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/ImageManager/TypeA/TypeA.tsx | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/common/ImageManager/TypeA/TypeA.tsx b/apps/web/src/components/common/ImageManager/TypeA/TypeA.tsx index 2160dd49..15fb4db9 100644 --- a/apps/web/src/components/common/ImageManager/TypeA/TypeA.tsx +++ b/apps/web/src/components/common/ImageManager/TypeA/TypeA.tsx @@ -20,21 +20,31 @@ export type ImageManagerTypeAProps = { * @default 5 */ maxFiles?: number; + onChange?: (files: File[]) => void; + value?: File[]; }; export const TypeA = ({ maxFileSize = 10, maxFiles = 5, + onChange, + value = [], }: ImageManagerTypeAProps) => { if (maxFileSize <= 0) throw new Error('maxFileSize는 0보다 커야합니다.'); if (maxFiles <= 0) throw new Error('maxFiles는 0보다 커야합니다.'); - const [images, setImages] = useState([]); - const isImageUploaded = images.length > 0; + const [images, setImages] = useState( + value.map((file) => ({ + id: crypto.randomUUID(), + file, + preview: URL.createObjectURL(file), + })) + ); + const toast = useToast(); + const handleUpload = useCallback( (files: FileList) => { - // 파일 크기 체크 const oversizedFiles = Array.from(files).filter( (file) => file.size > maxFileSize * 1024 * 1024 ); @@ -44,7 +54,6 @@ export const TypeA = ({ return; } - // 최대 파일 개수 체크 if (images.length + files.length > maxFiles) { toast.error( `이미지는 최대 ${maxFiles}장까지 업로드할 수 있어요.`, @@ -53,7 +62,6 @@ export const TypeA = ({ return; } - // 이미지 파일 타입 체크 const invalidFiles = Array.from(files).filter( (file) => !file.type.startsWith('image/') ); @@ -69,30 +77,28 @@ export const TypeA = ({ preview: URL.createObjectURL(file), })); - setImages((prev) => { - prev.forEach((image) => URL.revokeObjectURL(image.preview)); - return [...prev, ...newFiles]; - }); + const updatedImages = [...images, ...newFiles]; + + setImages(updatedImages); + onChange?.(updatedImages.map((img) => img.file)); // 🔹 변경: File 객체를 넘김 }, - [images.length, maxFiles, maxFileSize, toast] + [images, maxFiles, maxFileSize, toast, onChange] ); - const handleRemove = useCallback((id: string) => { - setImages((prevImages) => { - const targetImage = prevImages.find((image) => image.id === id); - if (targetImage) { - URL.revokeObjectURL(targetImage.preview); - } - return prevImages.filter((image) => image.id !== id); - }); - }, []); + const handleRemove = useCallback( + (id: string) => { + setImages((prevImages) => { + const updatedImages = prevImages.filter((image) => image.id !== id); + onChange?.(updatedImages.map((img) => img.file)); // 🔹 삭제 후 File 배열 업데이트 + return updatedImages; + }); + }, + [onChange] + ); - // 컴포넌트 언마운트 시 메모리 정리 useEffect(() => { return () => { - images.forEach((image) => { - URL.revokeObjectURL(image.preview); - }); + images.forEach((image) => URL.revokeObjectURL(image.preview)); }; }, [images]); @@ -101,22 +107,18 @@ export const TypeA = ({ value={{ images, onUpload: handleUpload, onRemove: handleRemove }} > -
+
이곳에 이미지를 드래그하거나 클릭하여 업로드 - {!isImageUploaded && ( + {images.length === 0 && ( - 최대 {maxFiles}장, 각 {maxFileSize}MB이하 + 최대 {maxFiles}장, 각 {maxFileSize}MB 이하 )}
- {isImageUploaded && ( + {images.length > 0 && (
From 7b325063a0bdce59c83c0cc350953c9f1e0b8b6e Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 07:54:32 +0900 Subject: [PATCH 17/21] =?UTF-8?q?refactor(apps/web):=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 189 ++++++++---------- .../AnimatedContainer.css.ts | 2 +- apps/web/src/app/create/constants.ts | 83 ++++++++ apps/web/src/app/create/types.ts | 18 ++ apps/web/src/app/page.tsx | 2 +- .../components/common/ImageManager/index.ts | 2 - .../MainBreadcrumbItem.css.ts | 28 +++ .../MainBreadcrumbItem/MainBreadcrumbItem.tsx | 18 ++ apps/web/src/components/common/index.ts | 4 + 9 files changed, 238 insertions(+), 108 deletions(-) create mode 100644 apps/web/src/app/create/constants.ts create mode 100644 apps/web/src/app/create/types.ts delete mode 100644 apps/web/src/components/common/ImageManager/index.ts create mode 100644 apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.css.ts create mode 100644 apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.tsx create mode 100644 apps/web/src/components/common/index.ts diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index 41149c18..dfcb8245 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -9,23 +9,20 @@ import { Icon, Button, } from '@repo/ui'; -import * as styles from './pageStyle.css'; +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 Link from 'next/link'; -import { isNil } from '@repo/ui/utils'; - -interface CreateFormValues { - topic: string; - purpose: 'INFORMATION' | 'OPINION' | 'HUMOR' | 'MARKETING'; - reference: 'NONE' | 'NEWS' | 'IMAGE'; - newsCategory?: string; - imageUrls?: string[]; - length: 'SHORT' | 'MEDIUM' | 'LONG'; - content: string; -} +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({ @@ -45,10 +42,26 @@ export default function Create() { 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' ? data.imageUrls : null, + imageUrls: data.reference === 'IMAGE' ? presignedUrls : null, }; console.log('폼 데이터:', requestData); @@ -61,12 +74,11 @@ export default function Create() {
- - - +
+ + 어떤 글을 생성할까요? + {/* 주제 */} @@ -106,33 +121,15 @@ export default function Create() { control={control} render={({ field: { onChange, value } }) => ( - } - > - 정보 제공 - - - } - > - 의견 표출 - - - } - > - 공감/유머 - - - } - > - 홍보/마케팅 - + {PURPOSE_OPTIONS.map(({ value, icon, label }) => ( + } + > + {label} + + ))} )} /> @@ -146,50 +143,50 @@ export default function Create() { control={control} render={({ field: { onChange, value } }) => ( - } - > - 입력된 주제로만 생성 - - 주제에 맞는 글을 간단히 생성 - - - } - > - 최근 뉴스로 글 생성 - - 최근 소식/뉴스 기반 - - - } - > - 이미지를 참고해 글 생성 - - 첨부한 이미지 기반 - - + {REFERENCE_OPTIONS.map( + ({ value, icon, label, description }) => ( + } + > + {label} + + {description} + + + ) + )} )} /> + {reference === REFERENCE_TYPE.IMAGE && ( + ( + + )} + /> + )}
{/* 조건부 렌더링 섹션들 */} - {reference === 'NEWS' && ( + {reference === REFERENCE_TYPE.NEWS && (
- - {['투자', '패션', '피트니스', '헬스케어']} - + ( + + {['투자', '패션', '피트니스', '헬스케어']} + + )} + />
)} - {reference === 'IMAGE' && <>ImageManager} - {/* 본문 길이 */}
@@ -198,27 +195,17 @@ export default function Create() { control={control} render={({ field: { onChange, value } }) => ( - - 누구나 이용 가능 - 짧은 게시물 - - 약 1~2문장, 최대 140자 - - - - X 유료 구독 전용 - 보통 게시물 - - 약 3~4문장, 최대 300자 - - - - X 유료 구독 전용 - 긴 게시물 - - 약 7~8문장, 최대 1000자 - - + {LENGTH_OPTIONS.map( + ({ value, label, description, badge }) => ( + + {badge} + {label} + + {description} + + + ) + )} )} /> @@ -246,9 +233,3 @@ export default function Create() { ); } - -type NullableString = string | null | undefined; - -function isEmptyStringOrNil(value: NullableString): boolean { - return isNil(value) || value.trim() === ''; -} diff --git a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts index ff89b17c..c72dc99d 100644 --- a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts +++ b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts @@ -5,7 +5,7 @@ export const containerStyle = style({ maxWidth: '92rem', margin: '0 auto', width: '100%', - minHeight: 'calc(100% + 12rem)', + height: 'calc(100% + 12rem)', padding: vars.space[32], borderRadius: '2.4rem 2.4rem 0 0', backgroundColor: vars.colors.grey, diff --git a/apps/web/src/app/create/constants.ts b/apps/web/src/app/create/constants.ts new file mode 100644 index 00000000..ead87027 --- /dev/null +++ b/apps/web/src/app/create/constants.ts @@ -0,0 +1,83 @@ +export const PURPOSE_TYPE = { + INFORMATION: 'INFORMATION', + OPINION: 'OPINION', + HUMOR: 'HUMOR', + MARKETING: 'MARKETING', +} as const; + +export const REFERENCE_TYPE = { + NONE: 'NONE', + NEWS: 'NEWS', + IMAGE: 'IMAGE', +} as const; + +export const LENGTH_TYPE = { + SHORT: 'SHORT', + MEDIUM: 'MEDIUM', + LONG: 'LONG', +} as const; + +export const PURPOSE_OPTIONS = [ + { + value: PURPOSE_TYPE.INFORMATION, + icon: 'document', + label: '정보 제공', + }, + { + value: PURPOSE_TYPE.OPINION, + icon: 'chat', + label: '의견 표출', + }, + { + value: PURPOSE_TYPE.HUMOR, + icon: 'smile', + label: '공감/유머', + }, + { + value: PURPOSE_TYPE.MARKETING, + icon: 'shopping', + label: '홍보/마케팅', + }, +] as const; + +export const REFERENCE_OPTIONS = [ + { + value: REFERENCE_TYPE.NONE, + icon: 'pencil', + label: '입력된 주제로만 생성', + description: '주제에 맞는 글을 간단히 생성', + }, + { + value: REFERENCE_TYPE.NEWS, + icon: 'stack', + label: '최근 뉴스로 글 생성', + description: '최근 소식/뉴스 기반', + }, + { + value: REFERENCE_TYPE.IMAGE, + icon: 'picture', + label: '이미지를 참고해 글 생성', + description: '첨부한 이미지 기반', + }, +] as const; + +export const LENGTH_OPTIONS = [ + { + value: LENGTH_TYPE.SHORT, + label: '짧은 게시물', + description: '약 1~2문장, 최대 140자', + badge: '누구나 이용 가능', + }, + { + value: LENGTH_TYPE.MEDIUM, + label: '보통 게시물', + description: '약 3~4문장, 최대 300자', + badge: 'X 유료 구독 전용', + }, + { + value: LENGTH_TYPE.LONG, + label: '긴 게시물', + description: '약 7~8문장, 최대 1000자', + badge: 'X 유료 구독 전용', + }, +] as const; diff --git a/apps/web/src/app/create/types.ts b/apps/web/src/app/create/types.ts new file mode 100644 index 00000000..c36b7af8 --- /dev/null +++ b/apps/web/src/app/create/types.ts @@ -0,0 +1,18 @@ +import { PURPOSE_TYPE, REFERENCE_TYPE, LENGTH_TYPE } from './constants'; + +export type PurposeType = (typeof PURPOSE_TYPE)[keyof typeof PURPOSE_TYPE]; + +export type ReferenceType = + (typeof REFERENCE_TYPE)[keyof typeof REFERENCE_TYPE]; + +export type LengthType = (typeof LENGTH_TYPE)[keyof typeof LENGTH_TYPE]; + +export interface CreateFormValues { + topic: string; + purpose: PurposeType; + reference: ReferenceType; + newsCategory?: string; + imageUrls?: File[]; + length: LengthType; + content: string; +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 13e76acd..f41fa856 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -18,7 +18,7 @@ import Link from 'next/link'; import { overlay } from 'overlay-kit'; import { useModal } from '@repo/ui/hooks'; import { useToast } from '@repo/ui/hooks'; -import { ImageManager } from '../components/common/ImageManager'; +import { ImageManager } from '@web/components/common'; type FormValues = { topic: string; diff --git a/apps/web/src/components/common/ImageManager/index.ts b/apps/web/src/components/common/ImageManager/index.ts deleted file mode 100644 index 814673ba..00000000 --- a/apps/web/src/components/common/ImageManager/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ImageManager } from './ImageManager'; -export type { ImageManagerTypeAProps } from './ImageManager'; diff --git a/apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.css.ts b/apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.css.ts new file mode 100644 index 00000000..a34a946e --- /dev/null +++ b/apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.css.ts @@ -0,0 +1,28 @@ +import { vars } from '@repo/theme'; +import { style } from '@vanilla-extract/css'; + +export const headerStyle = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + position: 'fixed', + top: 0, + left: 0, + right: 0, + padding: `${vars.space[12]} ${vars.space[24]}`, + zIndex: 1000, +}); + +export const insteadTextWrapperStyle = style({ + display: 'flex', + alignItems: 'center', + gap: vars.space[8], +}); + +export const insteadTextStyle = style({ + color: vars.colors.grey900, + fontSize: vars.typography.fontSize[24], + fontStyle: 'normal', + fontWeight: vars.typography.fontWeight.bold, + lineHeight: 'normal', +}); diff --git a/apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.tsx b/apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.tsx new file mode 100644 index 00000000..c6380954 --- /dev/null +++ b/apps/web/src/components/common/MainBreadcrumbItem/MainBreadcrumbItem.tsx @@ -0,0 +1,18 @@ +import { Icon } from '@repo/ui'; +import * as styles from './MainBreadcrumbItem.css'; +import Link from 'next/link'; + +type MainBreadcrumbItemProps = { + href?: string; +}; + +export function MainBreadcrumbItem({ + href = '/create', +}: MainBreadcrumbItemProps) { + return ( + + + Instead + + ); +} diff --git a/apps/web/src/components/common/index.ts b/apps/web/src/components/common/index.ts new file mode 100644 index 00000000..0d8917a0 --- /dev/null +++ b/apps/web/src/components/common/index.ts @@ -0,0 +1,4 @@ +export { ImageManager } from './ImageManager/ImageManager'; +export type { ImageManagerTypeAProps } from './ImageManager/ImageManager'; + +export { MainBreadcrumbItem } from './MainBreadcrumbItem/MainBreadcrumbItem'; From fac8081d2df213421b68942a11fe48c1ed1686eb Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 07:56:16 +0900 Subject: [PATCH 18/21] =?UTF-8?q?chore(apps/web):=20px->rem=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/ImageManager/TypeA/UploadedImages.css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/common/ImageManager/TypeA/UploadedImages.css.ts b/apps/web/src/components/common/ImageManager/TypeA/UploadedImages.css.ts index 067c1b1c..a9abeca7 100644 --- a/apps/web/src/components/common/ImageManager/TypeA/UploadedImages.css.ts +++ b/apps/web/src/components/common/ImageManager/TypeA/UploadedImages.css.ts @@ -31,8 +31,8 @@ export const removeButton = style({ border: 'transparent', cursor: 'pointer', ':focus-visible': { - outline: `2px solid ${vars.colors.primary500}`, - outlineOffset: '2px', + outline: `0.2rem solid ${vars.colors.primary500}`, + outlineOffset: '0.2rem', }, ':hover': { opacity: 0.8, From a5a0bc611ba95d6385039d75c722c5fb236781b4 Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 07:58:07 +0900 Subject: [PATCH 19/21] =?UTF-8?q?fix(app/web):=20defaultValues=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index dfcb8245..ab43e178 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -30,7 +30,7 @@ export default function Create() { topic: '', purpose: 'INFORMATION', reference: 'NONE', - newsCategory: undefined, // TODO: 백엔드로부터 받는 데이터 타입으로 수정 + newsCategory: '투자', // TODO: 백엔드로부터 받는 데이터 타입으로 수정 imageUrls: [], // TODO: presigned url 받아서 첨부 length: 'SHORT', content: '', From 351838f7bad17d3933729363efb7d9ff07c221c9 Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 08:03:22 +0900 Subject: [PATCH 20/21] =?UTF-8?q?fix(apps/web):=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/AnimatedContainer/AnimatedContainer.css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts index c72dc99d..f3dfe8cd 100644 --- a/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts +++ b/apps/web/src/app/create/_components/AnimatedContainer/AnimatedContainer.css.ts @@ -5,8 +5,8 @@ export const containerStyle = style({ maxWidth: '92rem', margin: '0 auto', width: '100%', - height: 'calc(100% + 12rem)', - padding: vars.space[32], + padding: `${vars.space[32]} ${vars.space[32]} 12rem ${vars.space[32]}`, + borderRadius: '2.4rem 2.4rem 0 0', backgroundColor: vars.colors.grey, }); From 3b38884f657909fae4903f7c45ed36bcd0e1e8a0 Mon Sep 17 00:00:00 2001 From: minseong Date: Sat, 1 Feb 2025 08:19:24 +0900 Subject: [PATCH 21/21] =?UTF-8?q?fix(apps/web):=20placeholder=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/create/Create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/create/Create.tsx b/apps/web/src/app/create/Create.tsx index ab43e178..55850491 100644 --- a/apps/web/src/app/create/Create.tsx +++ b/apps/web/src/app/create/Create.tsx @@ -221,7 +221,7 @@ export default function Create() { render={({ field }) => ( )}