Skip to content
4 changes: 3 additions & 1 deletion apps/client/src/shared/api/external/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
}
};
2 changes: 1 addition & 1 deletion apps/client/src/shared/api/file/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export interface DeleteFile {
fileName: string;
fileKey: string;
}
6 changes: 5 additions & 1 deletion apps/client/src/shared/api/file/upload/index.ts
Original file line number Diff line number Diff line change
@@ -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<ImageFile>('/file/upload', {
params: {
fileFormat: fileFormat,
},
});

return response.data.data;
};
11 changes: 7 additions & 4 deletions apps/client/src/shared/component/Modal/Footer/ModalFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('');

Expand Down Expand Up @@ -49,7 +49,7 @@ const WorkSpaceCategory = () => {
className="select-container"
/>
</Modal.Body>
<Modal.Footer step={2} type="create-workspace" onClick={handleNext} disabled={isDisabled} />
<Modal.Footer step={2} type="create-workspace" onClick={handleNext} onPrev={prevStep} disabled={isDisabled} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand All @@ -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();
},
Expand All @@ -42,17 +38,15 @@ const WorkSpaceComplete = () => {
<Modal.Header step={4} totalSteps={4} />
<Modal.Body>
<div css={{ width: '100%', paddingTop: '2rem' }}>
<picture>
<source srcSet={complete} />
<img
css={{ width: '30rem', height: '30rem', objectFit: 'cover' }}
src={completePng}
alt="워크 스페이스 생성 완료"
/>
</picture>
<img
css={{ width: '33.6rem', height: '33.8rem', objectFit: 'cover' }}
src={formData.fileUrl}
onError={(e) => (e.currentTarget.src = IMAGE_PLACEHOLDER)}
alt="워크 스페이스 생성 완료"
/>
</div>
</Modal.Body>
<Modal.Footer step={1} type="create-workspace" onClick={handleSave} disabled={false} />
<Modal.Footer step={4} type="create-workspace" onClick={handleSave} disabled={false} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,76 @@ 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<string>('');
const imgUploadInput = useRef<HTMLInputElement | null>(null);
const [fileURL, setFileURL] = useState('');
const [file, setFile] = useState<File | null>(null);

const imgUploadInput = useRef<HTMLInputElement | null>(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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

콘솔지워주세용ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 발생할 때 찍어주는 콘솔이라서 유지하겠슴니당 !

setFormData({ fileKey: '' });
},
}
);
};

const handleImageChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -24,7 +23,12 @@ const WorkSpaceImage = () => {
<Flex styles={{ width: '100%', paddingTop: '2rem', justify: 'center' }}>
<div css={[{ cursor: 'pointer' }, imageBoxStyle]}>
{fileURL ? (
<img src={fileURL} alt="프로필 이미지" css={imageAddStyle} />
<img
src={fileURL}
alt="프로필 이미지"
css={imageAddStyle}
onError={(e) => (e.currentTarget.src = IMAGE_PLACEHOLDER)}
/>
) : (
<Label id="imgUploadInput" css={imageAddStyle}>
<IcTeamProfileAdd width={200} height={200} />
Expand All @@ -42,7 +46,7 @@ const WorkSpaceImage = () => {
/>
</Flex>
</Modal.Body>
<Modal.Footer step={3} type="create-workspace" onClick={() => nextStep()} disabled={isDisabled} />
<Modal.Footer step={3} type="create-workspace" onPrev={prevStep} onClick={() => nextStep()} disabled={false} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ import { useState } from 'react';

import { Modal } from '@/shared/component/Modal';
import { inputWrapperStyle } from '@/shared/component/WorkSpaceModal/name/WorkSpaceName.style';
import { MAX_TEAM_NAME, WORKSPACE_MODAL } from '@/shared/constant';
import { useFunnel } from '@/shared/hook/common/useFunnel';
import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext';

const WorkSpaceName = () => {
const [inputValue, setInputValue] = useState('');

const { setFormData } = useWorkSpaceContext();
const { formData, setFormData } = useWorkSpaceContext();
const { nextStep } = useFunnel();

const [inputValue, setInputValue] = useState(formData.name);

const isDisabled = inputValue.trim().length === 0;
const isNameLengthError = inputValue.length > MAX_TEAM_NAME;

const handleNext = () => {
setFormData({ name: inputValue });
nextStep();
};

const isDisabled = inputValue.trim().length === 0;

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
Expand All @@ -32,6 +34,9 @@ const WorkSpaceName = () => {
placeholder="팀, 동아리, 조직 이름 입력"
value={inputValue}
onChange={handleInputChange}
isError={isNameLengthError}
supportingText={isNameLengthError ? WORKSPACE_MODAL.NAME : ''}
maxLength={MAX_TEAM_NAME}
css={inputWrapperStyle}
/>
</div>
Expand Down
8 changes: 8 additions & 0 deletions apps/client/src/shared/constant/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
16 changes: 8 additions & 8 deletions apps/client/src/shared/constant/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const MODAL_CONTENTS: Record<ModalContentType, ModalContent> = {
) : (
<span>{`${step}/${totalSteps}`}</span>
),
title: '워크스페이스 이름 입력',
infoText: '워크스페이스 이름을 입력해주세요.',
title: '새로운 워크스페이스 생성하기',
infoText: '워크스페이스의 이름을 입력해주세요',
},
{
icon: (step: number, totalSteps: number) =>
Expand All @@ -54,8 +54,8 @@ export const MODAL_CONTENTS: Record<ModalContentType, ModalContent> = {
) : (
<span>{`${step}/${totalSteps}`}</span>
),
title: '카테고리 선택',
infoText: '카테고리를 선택해주세요.',
title: '새로운 워크스페이스 생성하기',
infoText: '카테고리를 선택해주세요',
},
{
icon: (step: number, totalSteps: number) =>
Expand All @@ -64,8 +64,8 @@ export const MODAL_CONTENTS: Record<ModalContentType, ModalContent> = {
) : (
<span>{`${step}/${totalSteps}`}</span>
),
title: '프로필 이미지 등록',
infoText: '프로필 이미지를 등록해주세요.',
title: '동아리 프로필 이미지 등록',
infoText: '우리 동아리 프로필에 표시할 이미지를 등록해주세요',
},
{
icon: (step: number, totalSteps: number) =>
Expand All @@ -74,8 +74,8 @@ export const MODAL_CONTENTS: Record<ModalContentType, ModalContent> = {
) : (
<span>{`${step}/${totalSteps}`}</span>
),
title: '완료',
infoText: '워크스페이스 생성이 완료되었습니다.',
title: '워크스페이스 생성완료',
infoText: '이제 워크스페이스를 사용할 수 있습니다.',
},
],
buttons: [
Expand Down
Loading
Loading