diff --git a/apps/client/src/shared/api/external/index.ts b/apps/client/src/shared/api/external/index.ts index 3f844551..5b1a91e9 100644 --- a/apps/client/src/shared/api/external/index.ts +++ b/apps/client/src/shared/api/external/index.ts @@ -2,6 +2,8 @@ import mime from 'mime'; import axios from 'axios'; +import { getFileKey } from '@/shared/util/file'; + export const putUploadToS3 = async (presignedUrl: string, file: File) => { const contentType = mime.getType(file.name) || 'application/octet-stream'; @@ -15,6 +17,6 @@ export const putUploadToS3 = async (presignedUrl: string, file: File) => { // Presigned URL에서 쿼리 문자열을 제거하여 파일 URL을 생성 const uploadedFileUrl = presignedUrl.split('?')[0]; - return uploadedFileUrl; + return getFileKey(uploadedFileUrl); } }; diff --git a/apps/client/src/shared/api/file/type.ts b/apps/client/src/shared/api/file/type.ts index db1cbebf..90d90bfc 100644 --- a/apps/client/src/shared/api/file/type.ts +++ b/apps/client/src/shared/api/file/type.ts @@ -1,3 +1,3 @@ export interface DeleteFile { - fileName: string; + fileKey: string; } diff --git a/apps/client/src/shared/api/file/upload/index.ts b/apps/client/src/shared/api/file/upload/index.ts index a137f5a0..403635bf 100644 --- a/apps/client/src/shared/api/file/upload/index.ts +++ b/apps/client/src/shared/api/file/upload/index.ts @@ -1,10 +1,14 @@ +import { paths } from '@/shared/__generated__/schema'; import { axiosInstance } from '@/shared/api/instance'; +type ImageFile = paths['/api/v1/file/upload']['get']['responses']['200']['content']['*/*']; + export const getFile = async (fileFormat: string) => { - const response = await axiosInstance.get('/file/upload', { + const response = await axiosInstance.get('/file/upload', { params: { fileFormat: fileFormat, }, }); + return response.data.data; }; diff --git a/apps/client/src/shared/component/Modal/Footer/ModalFooter.tsx b/apps/client/src/shared/component/Modal/Footer/ModalFooter.tsx index ac8c9c7a..5e60ff3a 100644 --- a/apps/client/src/shared/component/Modal/Footer/ModalFooter.tsx +++ b/apps/client/src/shared/component/Modal/Footer/ModalFooter.tsx @@ -52,10 +52,13 @@ const ModalFooterButtons = ( switch (type) { case 'create-workspace': - return [ - step >= 3 ? createButton('건너뛰기', onClick, 'outline') : null, - createButton(step === 5 ? '확인' : '다음으로', onClick, 'primary', disabled), - ].filter(Boolean) as FooterButton[]; + if (step === 1) { + return [createButton('다음', onClick, 'primary', disabled)]; + } + if (step === 2 || step === 3) { + return [createButton('이전', onPrev, 'outline'), createButton('다음', onClick, 'primary', disabled)]; + } + return [createButton('확인', onClick, 'primary')]; case 'create-block': if (step === 1) { diff --git a/apps/client/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx b/apps/client/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx index 5eb836e2..c80204d3 100644 --- a/apps/client/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx +++ b/apps/client/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx @@ -12,7 +12,7 @@ type Category = components['schemas']['TeamCreateRequest']['category']; const WorkSpaceCategory = () => { const { setFormData } = useWorkSpaceContext(); - const { nextStep } = useFunnel(); + const { prevStep, nextStep } = useFunnel(); const [selected, setSelected] = useState(''); @@ -49,7 +49,7 @@ const WorkSpaceCategory = () => { className="select-container" /> - + ); }; diff --git a/apps/client/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx b/apps/client/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx index c85a492b..43a94f37 100644 --- a/apps/client/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx +++ b/apps/client/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx @@ -1,19 +1,17 @@ import { useQueryClient } from '@tanstack/react-query'; -import completePng from '@/common/asset/img/workspace_complete.png'; -import complete from '@/common/asset/img/workspace_complete.webp'; - import { $api } from '@/shared/api/client'; import { Modal } from '@/shared/component/Modal'; +import { IMAGE_PLACEHOLDER } from '@/shared/constant'; import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext'; import { useCloseModal } from '@/shared/store/modal'; const WorkSpaceComplete = () => { - const queryClient = useQueryClient(); - const closeModal = useCloseModal(); const { formData } = useWorkSpaceContext(); + + const queryClient = useQueryClient(); const { mutate: postTeamMutate } = $api.useMutation('post', '/api/v1/teams'); const handleSave = () => { @@ -22,14 +20,12 @@ const WorkSpaceComplete = () => { body: { name: formData.name, category: formData.category, - iconImageKey: formData.fileUrlData, + iconImageKey: formData.fileKey, }, }, { onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['get', '/api/v1/members/teams'], - }); + queryClient.invalidateQueries({ queryKey: ['get', '/api/v1/members/teams'] }); closeModal(); }, @@ -42,17 +38,15 @@ const WorkSpaceComplete = () => {
- - - 워크 스페이스 생성 완료 - + (e.currentTarget.src = IMAGE_PLACEHOLDER)} + alt="워크 스페이스 생성 완료" + />
- + ); }; diff --git a/apps/client/src/shared/component/WorkSpaceModal/hook/common/useImageUpload.tsx b/apps/client/src/shared/component/WorkSpaceModal/hook/common/useImageUpload.tsx index e502c3ec..2ef50a73 100644 --- a/apps/client/src/shared/component/WorkSpaceModal/hook/common/useImageUpload.tsx +++ b/apps/client/src/shared/component/WorkSpaceModal/hook/common/useImageUpload.tsx @@ -3,35 +3,39 @@ import { useRef, useState } from 'react'; import { $api } from '@/shared/api/client'; import { usePutUploadMutation } from '@/shared/component/TimeBlockModal/hook/api/usePutUploadMutation'; import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext'; -import { extractFileExtension, getFileKey } from '@/shared/util/file'; +import { extractFileExtension } from '@/shared/util/file'; const useImageUpload = () => { - const [fileURL, setFileURL] = useState(''); - const imgUploadInput = useRef(null); + const [fileURL, setFileURL] = useState(''); const [file, setFile] = useState(null); + const imgUploadInput = useRef(null); const fileExtension = file ? extractFileExtension(file.name) : ''; const { mutate: uploadToS3Mutate } = usePutUploadMutation(); const { mutate: deleteFileMutate } = $api.useMutation('post', '/api/v1/file'); - const { data: fileData, refetch: refetchFileData } = $api.useQuery('get', '/api/v1/file/upload', { - params: { query: { fileFormat: fileExtension }, options: { enabled: !!file } }, + const { data: fileData } = $api.useQuery('get', '/api/v1/file/upload', { + params: { query: { fileFormat: fileExtension } }, }); const { setFormData } = useWorkSpaceContext(); - const handleFileUpload = (selectedFile: File, presignedUrl: string) => { - const newFileURL = URL.createObjectURL(selectedFile); + const handleFileUpload = (fileData: File, presignedUrl: string) => { + const newFileURL = URL.createObjectURL(fileData); setFileURL(newFileURL); + uploadToS3Mutate( - { presignedUrl, file: selectedFile }, + { presignedUrl, file: fileData }, { - onSuccess: (uploadedFileUrl) => { - URL.revokeObjectURL(newFileURL); - if (uploadedFileUrl) { - setFileURL(uploadedFileUrl); - setFormData({ fileUrlData: uploadedFileUrl }); + onSuccess: (response) => { + if (response) { + setFormData({ fileKey: response }); } + URL.revokeObjectURL(newFileURL); + }, + onError: (error) => { + console.error(error); + setFormData({ fileKey: '' }); }, } ); @@ -39,37 +43,36 @@ const useImageUpload = () => { const handleImageChange = async (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; - if (selectedFile) { - setFileURL(URL.createObjectURL(selectedFile)); - setFile(selectedFile); - setFormData({ fileUrlData: '' }); - - const { data } = await refetchFileData(); - if (data?.data?.url) { - handleFileUpload(selectedFile, data.data.url); - } + if (!selectedFile) return; + + setFile(selectedFile); + setFileURL(URL.createObjectURL(selectedFile)); + setFormData({ fileKey: '', fileUrl: fileData?.data?.url }); + + if (fileData?.data?.url) { + handleFileUpload(selectedFile, fileData?.data?.url); } }; const handleImageRemove = () => { - if (fileData?.data?.fileKey) { - deleteFileMutate( - { - body: { fileKey: getFileKey(fileURL) }, + if (!fileData?.data?.fileKey) return; + + deleteFileMutate( + { body: { fileKey: fileData.data.fileKey } }, + { + onSuccess: () => { + URL.revokeObjectURL(fileURL); + + setFileURL(''); + setFile(null); + setFormData({ fileKey: '', fileUrl: '' }); + + if (imgUploadInput.current) { + imgUploadInput.current.value = ''; + } }, - { - onSuccess: () => { - URL.revokeObjectURL(fileURL); - setFileURL(''); - setFile(null); - setFormData({ fileUrlData: '' }); - if (imgUploadInput.current) { - imgUploadInput.current.value = ''; - } - }, - } - ); - } + } + ); }; return { diff --git a/apps/client/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx b/apps/client/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx index 51c4c7bc..2ea2d285 100644 --- a/apps/client/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx +++ b/apps/client/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx @@ -8,14 +8,13 @@ import { imageBoxStyle, imageDeleteStyle, } from '@/shared/component/WorkSpaceModal/image/WorkSpaceImage.style'; +import { IMAGE_PLACEHOLDER } from '@/shared/constant'; import { useFunnel } from '@/shared/hook/common/useFunnel'; const WorkSpaceImage = () => { const { fileURL, imgUploadInput, handleImageChange, handleImageRemove } = useImageUpload(); - const { nextStep } = useFunnel(); - - const isDisabled = !fileURL; + const { prevStep, nextStep } = useFunnel(); return ( <> @@ -24,7 +23,12 @@ const WorkSpaceImage = () => {
{fileURL ? ( - 프로필 이미지 + 프로필 이미지 (e.currentTarget.src = IMAGE_PLACEHOLDER)} + /> ) : (
diff --git a/apps/client/src/shared/constant/index.ts b/apps/client/src/shared/constant/index.ts index d845e411..12ae1354 100644 --- a/apps/client/src/shared/constant/index.ts +++ b/apps/client/src/shared/constant/index.ts @@ -111,4 +111,12 @@ export const IMAGE_MODAL = { export const MAX_TEAM_COUNT = 8; +export const MAX_TEAM_NAME = 30; + +export const WORKSPACE_MODAL = { + NAME: '워크스페이스명은 30자까지 작성 가능해요', +}; + +export const IMAGE_PLACEHOLDER = 'https://github.com/user-attachments/assets/7f97f986-6dc8-4900-9f53-3a2900565345'; + export const SEARCH_DELAY = 400; diff --git a/apps/client/src/shared/constant/modal.tsx b/apps/client/src/shared/constant/modal.tsx index ec2d0b6f..8237124e 100644 --- a/apps/client/src/shared/constant/modal.tsx +++ b/apps/client/src/shared/constant/modal.tsx @@ -44,8 +44,8 @@ export const MODAL_CONTENTS: Record = { ) : ( {`${step}/${totalSteps}`} ), - title: '워크스페이스 이름 입력', - infoText: '워크스페이스 이름을 입력해주세요.', + title: '새로운 워크스페이스 생성하기', + infoText: '워크스페이스의 이름을 입력해주세요', }, { icon: (step: number, totalSteps: number) => @@ -54,8 +54,8 @@ export const MODAL_CONTENTS: Record = { ) : ( {`${step}/${totalSteps}`} ), - title: '카테고리 선택', - infoText: '카테고리를 선택해주세요.', + title: '새로운 워크스페이스 생성하기', + infoText: '팀 카테고리를 선택해주세요', }, { icon: (step: number, totalSteps: number) => @@ -64,8 +64,8 @@ export const MODAL_CONTENTS: Record = { ) : ( {`${step}/${totalSteps}`} ), - title: '프로필 이미지 등록', - infoText: '프로필 이미지를 등록해주세요.', + title: '동아리 프로필 이미지 등록', + infoText: '우리 동아리 프로필에 표시할 이미지를 등록해주세요', }, { icon: (step: number, totalSteps: number) => @@ -74,8 +74,8 @@ export const MODAL_CONTENTS: Record = { ) : ( {`${step}/${totalSteps}`} ), - title: '완료', - infoText: '워크스페이스 생성이 완료되었습니다.', + title: '워크스페이스 생성완료', + infoText: '이제 워크스페이스를 사용할 수 있습니다.', }, ], buttons: [ diff --git a/apps/client/src/shared/hook/common/useWorkSpaceContext.tsx b/apps/client/src/shared/hook/common/useWorkSpaceContext.tsx index db251404..353abb6f 100644 --- a/apps/client/src/shared/hook/common/useWorkSpaceContext.tsx +++ b/apps/client/src/shared/hook/common/useWorkSpaceContext.tsx @@ -14,7 +14,8 @@ interface WorkSpaceFormData { | '종교' | '국제교류' | '네트워킹'; - fileUrlData: string; + fileKey: string; + fileUrl: string; } interface WorkSpaceContextType { @@ -37,7 +38,8 @@ export const WorkSpaceProvider = ({ children }: { children: ReactNode }) => { const [formData, setFormDataState] = useState({ name: '', category: '전체', - fileUrlData: '', + fileKey: '', + fileUrl: '', }); const setFormData = useCallback((data: Partial) => { @@ -48,7 +50,8 @@ export const WorkSpaceProvider = ({ children }: { children: ReactNode }) => { setFormDataState({ name: '', category: '전체', - fileUrlData: '', + fileKey: '', + fileUrl: '', }); }, []);