Skip to content

Commit

Permalink
Merge pull request #47 from JECT-Study/feature/artwork-upload
Browse files Browse the repository at this point in the history
[TASK-92, TASK-93, TASK-94] style: 작품 업로드 페이지 UI 구현
  • Loading branch information
dahyeo-n authored Jan 19, 2025
2 parents 084e927 + b6e70b3 commit e0d6456
Show file tree
Hide file tree
Showing 15 changed files with 437 additions and 51 deletions.
260 changes: 260 additions & 0 deletions src/app/artwork/upload/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
'use client';

import { ChangeEvent, useState } from 'react';

import OvalButton from '@/components/Button/OvalButton';
import FilterDropdown from '@/components/FilterDropdown';
import Icon from '@/components/Icon/Icon';
import BasicInput from '@/components/Input/BasicInput';
import ARTWORK_FIELDS from '@/constants/artworkFields';

import Textarea from '../../../components/Input/Textarea';

interface ArtworkFieldsErrors {
artworkTitleError?: string;
selectedArtworkFieldError?: string;
uploadedImageError?: string;
}

const MAX_TITLE_LENGTH = 50;
const MAX_DESCRIPTION_LENGTH = 1000;
const REQUIRED_FIELDS_ERROR_MESSAGE = '필수 항목입니다.';

const PRIVACY_SETTING_OPTIONS = [
{ name: '전체공개', value: 'PUBLIC' },
{ name: '비공개', value: 'PRIVATE' },
];

const ArtworkUpload = () => {
const [artworkTitle, setArtworkTitle] = useState('');
const [selectedArtworkField, setSelectedArtworkField] = useState('');
const [privacySetting, setPrivacySetting] = useState<'전체공개' | '비공개'>(
'전체공개',
);
const [uploadedImage, setUploadedImage] = useState<string>('');
const [artworkDescription, setArtworkDescription] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<ArtworkFieldsErrors>({
artworkTitleError: '',
selectedArtworkFieldError: '',
uploadedImageError: '',
});

const clearErrorMessage = (targetField: string) => {
setErrors((prevErrors) => ({ ...prevErrors, [targetField]: '' }));
};

const handleArtworkTitleChanged = (e: ChangeEvent<HTMLInputElement>) => {
setArtworkTitle(e.target.value);

if (errors.artworkTitleError) clearErrorMessage('artworkTitleError');
};

const handleArtworkDescriptionOnChange = (
e: ChangeEvent<HTMLTextAreaElement>,
) => {
setArtworkDescription(e.target.value);
};

const handleArtworkFieldClick = (artworkField: string) => {
setSelectedArtworkField(artworkField);

if (!artworkTitle.trim()) {
setErrors((prevErrors) => ({
...prevErrors,
artworkTitleError: REQUIRED_FIELDS_ERROR_MESSAGE,
}));
} else {
clearErrorMessage('artworkTitleError');
}

if (errors.selectedArtworkFieldError)
clearErrorMessage('selectedArtworkFieldError');
};

const handlePrivacySettingChange = (
newPrivacySetting: '전체공개' | '비공개',
) => {
setPrivacySetting(newPrivacySetting);
};

const handleImageUpload = () => {
// TODO: 실제 이미지 업로드 로직 구현
setUploadedImage('uploaded-image-url'); // 테스트용

if (errors.uploadedImageError) clearErrorMessage('uploadedImageError');
};

const validateArtworkUploadForm = () => {
const newErrors: ArtworkFieldsErrors = {};

if (!artworkTitle.trim())
newErrors.artworkTitleError = REQUIRED_FIELDS_ERROR_MESSAGE;
if (!selectedArtworkField.trim())
newErrors.selectedArtworkFieldError = REQUIRED_FIELDS_ERROR_MESSAGE;
if (!uploadedImage)
newErrors.uploadedImageError = REQUIRED_FIELDS_ERROR_MESSAGE;
setErrors(newErrors);

return Object.keys(newErrors).length === 0;
};

const isRequiredFieldsValid =
artworkTitle && selectedArtworkField && uploadedImage && !isSubmitting;

const handleScrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};

const handleRequiredFieldsNotFilledOut = () => {
validateArtworkUploadForm();
handleScrollToTop();
};

const handleArtworkUpload = () => {
setIsSubmitting(true);

console.log('업로드 완료'); // [테스트용] 업로드 처리 (API 호출)
// TODO: 작성한 글 상세 페이지로 이동
};

return (
<div className='max-w-[1920px] m-auto px-[36px] py-[70px] lg:px-[140px]'>
<h1>작품 업로드</h1>
<div className='pt-[70px] pb-[58px] md:pb-[40px]'>
<BasicInput
type='text'
label='작품 제목'
placeholder='작품 제목을 입력하세요.'
value={artworkTitle}
onChange={handleArtworkTitleChanged}
showTextLength={true}
maxLength={MAX_TITLE_LENGTH}
isInvalid={!!errors.artworkTitleError}
errorMessage={errors.artworkTitleError}
/>
</div>

<div className='flex flex-col md:flex-row items-start self-stretch gap-[38px] md:gap-[50px] pb-[78px]'>
<FilterDropdown
label='작품 카테고리'
placeholder='카테고리 선택'
options={ARTWORK_FIELDS.map((field) => field.name)}
selected={selectedArtworkField}
onChange={(value) => handleArtworkFieldClick(value)}
isInvalid={!!errors.selectedArtworkFieldError}
errorMessage={errors.selectedArtworkFieldError}
className='w-full'
/>

<FilterDropdown
label='공개범위'
options={PRIVACY_SETTING_OPTIONS.map((option) => option.name)}
selected={privacySetting}
onChange={() => handlePrivacySettingChange(privacySetting)}
className='w-full'
/>
</div>

<div className='relative pb-[70px]'>
<div
className='flex flex-col justify-center items-center gap-[15px] p-[140px] h-[511px] md:h-[853px]'
style={{
border: '2px dashed transparent',
borderImage:
'repeating-linear-gradient(45deg, gray 0, gray 10px, transparent 10px, transparent 20px) 1',
}}
>
<p className='body2 text-center text-gray-500 self-stretch'>
첨부할 작품 이미지를 끌어오거나,
<br />
작품 업로드 버튼을 눌러 이미지를 선택하세요.
</p>
<OvalButton
variant='primary'
buttonSize='s'
onClick={handleImageUpload}
>
<Icon name='UploadShare' size='m' className='mr-2.5' />
이미지 업로드
</OvalButton>
<p className='button-s text-center text-gray-500'>
작품 이미지는 1장만 업로드 가능합니다.
</p>

{errors.uploadedImageError && (
<div className='flex items-center'>
<Icon
name='AlertCircle'
size='s'
className='text-system-error mr-2'
/>
<p className='button-s text-system-error'>
{errors.uploadedImageError}
</p>
</div>
)}

{uploadedImage && (
<img
src={uploadedImage}
alt='Uploaded Artwork'
className='mt-4 max-h-[200px] object-contain'
/>
)}

<button
aria-label='Button to change artwork image'
className='flex justify-center items-center w-[57px] h-[57px] md:w-[77px] md:h-[77px]
absolute right-[30px] bottom-[30px] rounded-full
bg-[rgba(35,34,37,0.5)] backdrop-blur-[12px]
shadow-lg hover:bg-[rgba(35,34,37,0.7)] transition'
>
<Icon name='Image' size='m' className='block md:hidden' />
<Icon name='Image' size='l' className='hidden md:block' />
</button>
</div>
</div>

<Textarea
label='작품 설명'
placeholder='작품 설명을 입력하세요.'
value={artworkDescription}
onChange={handleArtworkDescriptionOnChange}
showTextLength={true}
maxLength={MAX_DESCRIPTION_LENGTH}
/>

<div className='pt-[30px] flex justify-end items-center gap-[20px]'>
<button
className='button-s text-gray-300 flex items-center justify-center rounded-full
gap-[10px] px-[28px] leading-[50px]
transition-all duration-300 ease-in-out active:scale-95 hover:opacity-70'
>
취소
</button>

{!isSubmitting && isRequiredFieldsValid ? (
<OvalButton
variant='primary'
buttonSize='s'
onClick={handleArtworkUpload}
>
업로드
</OvalButton>
) : (
<button
onClick={handleRequiredFieldsNotFilledOut}
className='button-s text-gray-300 bg-gray-700 px-[28px] leading-[50px]
flex items-center justify-center rounded-full gap-[10px]
transition-all duration-300 ease-in-out active:scale-95 hover:opacity-70 hover:cursor-not-allowed'
>
업로드
</button>
)}
</div>
</div>
);
};

export default ArtworkUpload;
29 changes: 11 additions & 18 deletions src/components/ArtworkPage/ArtworkFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ArtworkField } from '@/types';

import ARTWORK_FIELDS from '../../constants/artworkFields';
import OvalButton from '../Button/OvalButton';
import DefaultCarousel from '../Carousel/DefaultCarousel';
import FilterDropdown from '../FilterDropdown';
Expand All @@ -10,23 +13,11 @@ interface ArtworkFilterProps {
setCurrentPage: (value: number | ((prev: number) => number)) => void;
}

interface ArtworkField {
name: string;
value: string;
}

const ARTWORK_FIELDS = [
const ARTWORK_FIELDS_WITH_ALL_OPTION = [
{ name: '전체', value: 'ALL' },
{ name: '회화', value: 'PAINTING' },
{ name: '공예/조각', value: 'CRAFTSCULPTURE' },
{ name: '드로잉', value: 'DRAWING' },
{ name: '판화', value: 'PRINTMAKING' },
{ name: '서예', value: 'CALLIGRAPHY' },
{ name: '일러스트', value: 'ILLUSTRATION' },
{ name: '디지털아트', value: 'DIGITALART' },
{ name: '사진', value: 'PHOTOGRAPHY' },
{ name: '기타', value: 'OTHERS' },
...ARTWORK_FIELDS,
];

const FILTER_OPTIONS = ['최신순', '인기순', '조회순'];

const ArtworkFilter = ({
Expand All @@ -37,8 +28,9 @@ const ArtworkFilter = ({
setCurrentPage,
}: ArtworkFilterProps) => {
const selectedArtworkFieldName =
ARTWORK_FIELDS.find((field) => field.value === selectedArtworkField)
?.name || '전체';
ARTWORK_FIELDS_WITH_ALL_OPTION.find(
(field) => field.value === selectedArtworkField,
)?.name || '전체';

const handleArtworkFieldClick = (artworkField: string) => {
setSelectedArtworkField(artworkField);
Expand All @@ -53,7 +45,7 @@ const ArtworkFilter = ({
<>
<div className='flex w-full justify-between items-end pb-[59px] overflow-x-auto'>
<DefaultCarousel
slides={ARTWORK_FIELDS}
slides={ARTWORK_FIELDS_WITH_ALL_OPTION}
renderSlide={(artworkField: ArtworkField) => (
<OvalButton
key={artworkField.value}
Expand All @@ -77,6 +69,7 @@ const ArtworkFilter = ({
options={FILTER_OPTIONS}
selected={selectedFilter}
onChange={handleFilterChange}
className='w-[149px]'
/>
</div>
</>
Expand Down
12 changes: 8 additions & 4 deletions src/components/Button/OvalButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ const OvalButton = ({
variant = 'primary',
buttonSize,
children,

onClick,

className,
disabled,
ariaLabel,
}: OvalButtonProps) => {
const bgColorClasses = {
Expand All @@ -27,21 +29,23 @@ const OvalButton = ({
};

const buttonSizeClasses = {
m: 'h-[69px] px-[46px] py-[17px]',
s: 'h-[50px] px-[34px] py-[20px]',
m: 'px-[46px] leading-[69px]',
s: 'px-[28px] leading-[50px]',
};

return (
<button
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel}
className={`
flex items-center justify-center rounded-full gap-[10px]
transition-all duration-300 ease-in-out active:scale-95 w-full
transition-all duration-300 ease-in-out active:scale-95
${bgColorClasses[variant]}
${hoverBgColorClasses[variant]}
${textColorClasses[variant]}
${buttonSizeClasses[buttonSize]}
${className}
`}
>
{buttonSize === 'm' ? (
Expand Down
Loading

0 comments on commit e0d6456

Please sign in to comment.