Skip to content

Commit

Permalink
[Feat] QFEED-117 토론방 생성 페이지 api 연동 (#60)
Browse files Browse the repository at this point in the history
* feat: qfeed-117 취미 카테고리 type 변경 (string->number)

* feat: qfeed-117 토론방 생성 페이지 api 연동

* refactor: qfeed-117 formatlastupdated 함수 수정
  • Loading branch information
se0kcess authored Dec 6, 2024
1 parent d96b2cd commit f7f8ee4
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useState } from 'react';

// 이미지 import
import travelImg from '@/assets/images/airplane.jpg';
import sportsImg from '@/assets/images/sports.jpg';
import fashionImg from '@/assets/images/fashion.jpg';
import cultureImg from '@/assets/images/culture.jpg';
import matzipImg from '@/assets/images/matzip.jpg';
import etcImg from '@/assets/images/etc.jpg';

import {
CategoryCard,
CategoryIcon,
Expand All @@ -18,29 +18,29 @@ import {
} from '@/components/ui/CategorySelectContainer/CategorySelectContainer.styles';

interface CategoryItem {
id: string;
id: number;
koreanName: string;
englishName: string;
image: string;
}

const categories: CategoryItem[] = [
{ id: 'travel', koreanName: '여행', englishName: 'Travel', image: travelImg },
{ id: 'sports', koreanName: '스포츠', englishName: 'Sports', image: sportsImg },
{ id: 'fashion', koreanName: '패션', englishName: 'Fashion', image: fashionImg },
{ id: 'culture', koreanName: '문화', englishName: 'Culture', image: cultureImg },
{ id: 'restaurant', koreanName: '맛집', englishName: 'Matzip', image: matzipImg },
{ id: 'etc', koreanName: '기타', englishName: 'Etc', image: etcImg },
{ id: 1, koreanName: '여행', englishName: 'Travel', image: travelImg },
{ id: 2, koreanName: '스포츠', englishName: 'Sports', image: sportsImg },
{ id: 3, koreanName: '패션', englishName: 'Fashion', image: fashionImg },
{ id: 4, koreanName: '문화', englishName: 'Culture', image: cultureImg },
{ id: 5, koreanName: '맛집', englishName: 'Matzip', image: matzipImg },
{ id: 6, koreanName: '기타', englishName: 'Etc', image: etcImg },
];

interface CategorySelectContainerProps {
onCategorySelect?: (categoryId: string) => void;
onCategorySelect?: (categoryId: number) => void;
}

const CategorySelectContainer = ({ onCategorySelect }: CategorySelectContainerProps) => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);

const handleCategoryClick = (categoryId: string) => {
const handleCategoryClick = (categoryId: number) => {
setSelectedCategory(categoryId);
onCategorySelect?.(categoryId);
};
Expand Down
18 changes: 15 additions & 3 deletions src/components/ui/ImageUpload/ImageUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ import {
} from '@/components/ui/ImageUpload/ImageUpload.styles';

interface ImageUploadProps {
onImageUpload?: (file: File | null) => void; // 이미지 업로드 이벤트 콜백
onImageUpload?: (file: File | null) => void;
accept?: string;
maxSize?: number;
}

export const ImageUpload: React.FC<ImageUploadProps> = ({ onImageUpload }) => {
export const ImageUpload = ({
onImageUpload,
accept = 'image/*',
maxSize = 5 * 1024 * 1024,
}: ImageUploadProps) => {
const [preview, setPreview] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

Expand All @@ -26,6 +32,12 @@ export const ImageUpload: React.FC<ImageUploadProps> = ({ onImageUpload }) => {
setError('이미지 파일(JPEG, PNG, GIF, WEBP)만 허용됩니다.');
return;
}

if (file.size > maxSize) {
setError(`파일 크기는 ${Math.floor(maxSize / (1024 * 1024))}MB 이하여야 합니다.`);
return;
}

const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
Expand Down Expand Up @@ -66,7 +78,7 @@ export const ImageUpload: React.FC<ImageUploadProps> = ({ onImageUpload }) => {
onClick={handleBoxClick}
hasPreview={!!preview}
>
<input type="file" accept="image/*" onChange={handleFileChange} />
<input type="file" accept={accept} onChange={handleFileChange} />
{preview ? (
<PreviewContainer>
<PreviewImage src={preview} alt="Uploaded Preview" />
Expand Down
6 changes: 3 additions & 3 deletions src/pages/QSpace/QSpacePost/CategorySelectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import { useState } from 'react';
import { useNavigate } from 'react-router';

const CategorySelectPage = () => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
const navigate = useNavigate();

const handleCategorySelect = (categoryId: string) => {
const handleCategorySelect = (categoryId: number) => {
setSelectedCategory(categoryId);
};

const handleNext = () => {
navigate('/qspace/post');
navigate('/qspace/post', { state: { categoryId: selectedCategory } });
};

return (
Expand Down
4 changes: 3 additions & 1 deletion src/pages/QSpace/QSpacePost/PostGroupPage.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from '@emotion/styled';

export const Container = styled.div`
background-color: ${theme.colors.background};
padding-bottom: 6rem;
height: 100vh;
position: relative;
`;

Expand All @@ -15,11 +15,13 @@ export const Header = styled.header`
`;

export const Content = styled.main`
background-color: ${theme.colors.background};
padding: 0 1.5rem;
display: flex;
flex-direction: column;
gap: 3rem;
margin-top: 2rem;
padding-bottom: 6rem;
`;

export const InputWrapper = styled.div`
Expand Down
36 changes: 21 additions & 15 deletions src/pages/QSpace/QSpacePost/PostGroupPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { Input, Textarea } from '@chakra-ui/react';

import theme from '@/styles/theme';
import BackButton from '@/components/ui/BackButton/BackButton';
import { ImageUpload } from '@/components/ui/ImageUpload/ImageUpload';

import {
CharCount,
Container,
Expand All @@ -11,22 +12,19 @@ import {
Header,
InputWrapper,
Label,
} from '@/pages/QSpace/QSpacePost/PostGroupPage.styles';
import { useNavigate } from 'react-router';
} from './PostGroupPage.styles';

import { createGroup } from '@/pages/QSpace/utils/createGroup';
import { useGroupForm } from '@/pages/QSpace/hooks/usePostGroupForm';

const PostGroupPage = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [imageFile, setImageFile] = useState<File | null>(null);
const navigate = useNavigate();
const { formData, formActions, formState, toast, navigate } = useGroupForm();
const { title, description } = formData;
const { setTitle, setDescription, setImageFile } = formActions;
const { isPending, setIsPending } = formState;

const handleCreateGroup = () => {
navigate('/qspace');
};

const handleImageUpload = (file: File | null) => {
console.log('Uploading image:', imageFile);
setImageFile(file);
createGroup({ formData, setIsPending, toast, navigate });
};

return (
Expand All @@ -46,6 +44,7 @@ const PostGroupPage = () => {
borderRadius={12}
maxLength={24}
bg="white"
placeholder="방 제목을 입력해주세요"
/>
<CharCount>{title.length}/24</CharCount>
</InputWrapper>
Expand All @@ -61,13 +60,14 @@ const PostGroupPage = () => {
borderRadius={12}
bg="white"
resize="none"
placeholder="그룹에 대한 설명을 입력해주세요"
/>
<CharCount>{description.length}/300</CharCount>
</InputWrapper>

<InputWrapper>
<Label>사진을 등록해주세요</Label>
<ImageUpload onImageUpload={handleImageUpload} />
<ImageUpload onImageUpload={setImageFile} />
</InputWrapper>

<CreateButton
Expand All @@ -77,8 +77,14 @@ const PostGroupPage = () => {
width="100%"
height="3.5rem"
onClick={handleCreateGroup}
isPending={isPending}
isDisabled={!title || !description || isPending}
_disabled={{
bg: theme.colors.gray[300],
cursor: 'not-allowed',
}}
>
방 만들기
{isPending ? '생성 중...' : '방 만들기'}
</CreateButton>
</Content>
</Container>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/QSpace/api/fetchGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const groupAPI = {
getGroupDetail: (groupId: number) => apiClient.get<Group>(`/groups/${groupId}`),

// 새로운 그룹 생성
createGroup: (data: CreateGroupRequest) => apiClient.post<Group>('/groups', data),
createGroup: (data: CreateGroupRequest) => apiClient.post<Group>('/groups/create', data),

// 그룹 정보 수정
updateGroup: ({ groupId, ...data }: UpdateGroupRequest) =>
Expand Down
15 changes: 15 additions & 0 deletions src/pages/QSpace/api/imageUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { apiClient } from '@/api/fetch';
import { UploadResponse } from '@/pages/QSpace/types/group';

export const uploadAPI = {
uploadImage: (file: File) => {
const formData = new FormData();
formData.append('file', file);

return apiClient.post<UploadResponse>('/upload/image', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
};
42 changes: 42 additions & 0 deletions src/pages/QSpace/hooks/usePostGroupForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useToast } from '@chakra-ui/react';
import { UseGroupFormReturn, GroupFormData } from '../types/group';

export const useGroupForm = (): UseGroupFormReturn => {
const location = useLocation();
const navigate = useNavigate();
const toast = useToast();
const { categoryId } = location.state as { categoryId: number };

const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [imageFile, setImageFile] = useState<File | null>(null);
const [isPending, setIsPending] = useState(false);

const formData: GroupFormData = {
title,
description,
imageFile,
categoryId,
};

const formActions = {
setTitle,
setDescription,
setImageFile,
};

const formState = {
isPending,
setIsPending,
};

return {
formData,
formActions,
formState,
toast,
navigate,
};
};
37 changes: 36 additions & 1 deletion src/pages/QSpace/types/group.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useToast } from '@chakra-ui/react';
import { NavigateFunction } from 'react-router';
export interface Group {
groupId: number;
url: string;
Expand All @@ -13,9 +15,42 @@ export interface CreateGroupRequest {
description: string;
categoryId: number;
url: string;
isOpen: true;
isOpen: boolean;
}

export interface UploadResponse {
imageUrl: string;
}

export interface UpdateGroupRequest extends Partial<CreateGroupRequest> {
groupId: number;
}

export interface GroupFormData {
title: string;
description: string;
imageFile: File | null;
categoryId: number;
}

export interface CreateGroupParams {
formData: GroupFormData;
setIsPending: (value: boolean) => void;
toast: ReturnType<typeof useToast>;
navigate: NavigateFunction;
}

export interface UseGroupFormReturn {
formData: GroupFormData;
formActions: {
setTitle: (value: string) => void;
setDescription: (value: string) => void;
setImageFile: (file: File | null) => void;
};
formState: {
isPending: boolean;
setIsPending: (value: boolean) => void;
};
toast: ReturnType<typeof useToast>;
navigate: NavigateFunction;
}
46 changes: 46 additions & 0 deletions src/pages/QSpace/utils/createGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { groupAPI } from '@/pages/QSpace/api/fetchGroups';
import { CreateGroupParams, CreateGroupRequest } from '@/pages/QSpace/types/group';
import { showErrorToast, showSuccessToast, uploadImage } from '@/pages/QSpace/utils/uploadImage';

export const createGroup = async ({
formData,
setIsPending,
toast,
navigate,
}: CreateGroupParams) => {
const { title, description, imageFile, categoryId } = formData;

if (!title || !description) {
toast({
title: '입력 확인',
description: '방 제목과 설명을 입력해주세요.',
status: 'warning',
duration: 3000,
isClosable: true,
});
return;
}

try {
setIsPending(true);
const createGroupData: Partial<CreateGroupRequest> = {
groupName: title,
description,
categoryId,
isOpen: true,
};

if (imageFile) {
const imageUrl = await uploadImage(imageFile, toast);
if (imageUrl) createGroupData.url = imageUrl;
}

await groupAPI.createGroup(createGroupData as CreateGroupRequest);
showSuccessToast(toast);
navigate('/qspace');
} catch (error) {
showErrorToast(toast, error);
} finally {
setIsPending(false);
}
};
Loading

0 comments on commit f7f8ee4

Please sign in to comment.