Skip to content

Commit

Permalink
Feat/#166: 글쓰기 시 사진을 추가 (#168)
Browse files Browse the repository at this point in the history
* chore: Browser Image Compression install

* chore: import order 옵션 변경

* feat: Browser Image 업로드 압축 설정 및 기능 추가

* feat: 이미지 업로드 및 상태 관리

* feat: 이미지 업로드 api 구현 및 연동

서버 에러 확인중

* chore: vscode setting json 추가

사용하지 않는 import 저장 시 자동 제거
import 누락 자동 import 기능 추가

* refactor: test data 제거

* feat: 글 수정 시 사진 수정 기능 추가

---------

Co-authored-by: yogjin <[email protected]>
  • Loading branch information
semnil5202 and yogjin authored Jun 26, 2024
1 parent f93c708 commit e18743c
Show file tree
Hide file tree
Showing 16 changed files with 916 additions and 231 deletions.
8 changes: 7 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ module.exports = {
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'type'],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
pathGroups: [
{
group: 'internal',
position: 'after',
},
],
alphabetize: {
order: 'asc',
caseInsensitive: true,
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
Expand Down
15 changes: 15 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit",
"source.addMissingImports": "explicit"
}
}
59 changes: 59 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@tanstack/react-query": "^5.17.19",
"@toss/use-overlay": "^1.3.8",
"axios": "^1.5.1",
"browser-image-compression": "^2.0.2",
"concept-be-design-system": "^0.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
18 changes: 0 additions & 18 deletions src/pages/FeedDetail/hooks/queries/useFeedDetailQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,6 @@ const useFeedDetailQuery = (id: string) => {
const { data: feedDetail } = useSuspenseQuery({
queryKey: ['feed', 'detail', id],
queryFn: () => getFeedDetail(id),
select: (data) => ({
...data,
imageResponses: [
{
id: 1,
imageUrl: 'https://www.contestkorea.com/admincenter/files/meet/202207070917022079123.jpg',
},
{
id: 2,
imageUrl: 'https://news.nateimg.co.kr/orgImg/sh/2022/11/18/6812837_996935_3331.jpg',
},
{
id: 3,
imageUrl:
'https://www.syu.ac.kr/wp-content/uploads/2020/07/%EA%B3%B5%EB%AA%A8%EC%A0%84-%ED%8F%AC%EC%8A%A4%ED%84%B0-scaled.jpg',
},
],
}),
});

return feedDetail;
Expand Down
48 changes: 33 additions & 15 deletions src/pages/Write/Write.page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import styled from '@emotion/styled';
import {
useCheckbox,
useRadio,
BottomSheet,
Box,
CheckboxContainer,
Divider,
Flex,
RadioContainer,
Spacer,
Text,
theme,
SVGAdd24,
SVGHeaderCheck24,
SVGCancel,
SVGHeaderCheck24,
SVGRadioCheck24,
SVGRadioUncheck24,
Flex,
Spacer,
Text,
theme,
useCheckbox,
useDropdown,
Box,
useRadio,
} from 'concept-be-design-system';
import { useState } from 'react';

import SEOMeta from '../../components/SEOMeta/SEOMeta';
import useAlert from '../../hooks/useAlert';
import AddImages from './components/AddImages';
import Header from './components/Header';
import RecruitmentPlaceSection from './components/RecruitmentPlaceSection';
import TitleAndIntroduceSection from './components/TitleAndIntroduceSection';
import { usePostIdeasMutation } from './hooks/mutations/usePostIdeasMutation';
import { useWritingInfoQuery } from './hooks/queries/useWritingInfoQuery';
import { Info } from './types';
import { Info, PostIdeasRequest } from './types';
import { get2DepthCountsBy1Depth } from './utils/get2DepthCountsBy1Depth';
import SEOMeta from '../../components/SEOMeta/SEOMeta';
import useAlert from '../../hooks/useAlert';

const WritePage = () => {
const openAlert = useAlert();
Expand All @@ -40,6 +40,7 @@ const WritePage = () => {
const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false);
const [selectedTeamRecruitment1Depth, setSelectedTeamRecruitment1Depth] = useState(skillCategoryResponses[0].name);
const [selectedSkillResponses, setSelectedSkillResponses] = useState<Info[]>([]);
const [images, setImages] = useState<File[]>([]);

const { checkboxValue, selectedCheckboxId, onChangeCheckbox } = useCheckbox({
branches,
Expand Down Expand Up @@ -89,15 +90,27 @@ const WritePage = () => {
return;
}

postIdeas({
const formData = new FormData();
const userData = {
title,
introduce,
recruitmentPlaceId: recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id || 1,
cooperationWay: selectedRadioName.cooperationWays,
branchIds: selectedCheckboxId.branches,
purposeIds: selectedCheckboxId.purposes,
skillCategoryIds: selectedSkillResponses.map((selectedSkillResponse) => selectedSkillResponse.id),
};

images.forEach((image) => {
formData.append('images', image);
});

const stringifiedUserData = JSON.stringify(userData);
const userDataBlob = new Blob([stringifiedUserData], { type: 'application/json' });

formData.append('request', userDataBlob);

postIdeas(formData as PostIdeasRequest);
};

const handleTitleChange = (newTitle: string) => {
Expand Down Expand Up @@ -147,7 +160,12 @@ const WritePage = () => {
onIntroduceChange={handleIntroduceChange}
/>

<Divider color="bg1" height={8} bottom={30} />
<Divider color="bg1" height={8} />

<AddImages images={images} setImages={setImages} />

<Divider color="bg1" height={8} />

<BottomWrapper>
<Box>
<CheckboxContainer
Expand Down Expand Up @@ -287,7 +305,7 @@ const MainWrapper = styled.div`
`;

const BottomWrapper = styled.div`
padding: 0px 22px;
padding: 30px 22px 0;
display: flex;
flex-direction: column;
gap: 35px;
Expand Down
97 changes: 97 additions & 0 deletions src/pages/Write/components/AddImages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import styled from '@emotion/styled';
import { Box, Flex, SVGCancel, theme } from 'concept-be-design-system';
import { useState } from 'react';
import useAlert from '../../../hooks/useAlert';
import useCompressImage from '../hooks/useCompressImage';

interface Props {
images: File[];
setImages: React.Dispatch<React.SetStateAction<File[]>>;
}

const AddImages = ({ images, setImages }: Props) => {
const [imageUrls, setImageUrls] = useState<string[]>([]);
const openAlert = useAlert();
const { compressImages } = useCompressImage();

const onChangeImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
const currentImages = e.target.files;

if (!currentImages) return;
if (images.length + currentImages.length > 3) {
openAlert({ content: '이미지는 최대 3개까지 업로드 가능합니다.' });
return;
}

const compressedImages = await compressImages(currentImages);
const imageObjectUrls = compressedImages.map((image) => URL.createObjectURL(image));

setImages([...images, ...compressedImages]);
setImageUrls([...imageUrls, ...imageObjectUrls]);
};

const onClickDeleteImage = (index: number) => {
setImages(images.filter((_, idx) => idx !== index));
setImageUrls(imageUrls.filter((_, idx) => idx !== index));
};

return (
<>
<Wrapper width="100%" height="100%" padding="22px" overflow="scroll" boxSizing="border-box">
<Flex gap={8}>
<AddImageLabel htmlFor="add-image">+</AddImageLabel>
<AddImageInput id="add-image" type="file" multiple onChange={onChangeImage}></AddImageInput>
{imageUrls.map((imageUrl, index) => (
<Box position="relative" key={index}>
<Flex
position="absolute"
top="0"
right="0"
width={32}
height={32}
justifyContent="center"
alignItems="center"
backgroundColor="b6"
cursor="pointer"
>
<SVGCancel color="white" onClick={() => onClickDeleteImage(index)} />
</Flex>
<Image src={imageUrl} alt={`이미지 ${index + 1}`} />
</Box>
))}
</Flex>
</Wrapper>
</>
);
};

const Wrapper = styled(Flex)`
&::-webkit-scrollbar {
display: none;
}
`;

const Image = styled.img`
width: 120px;
height: 120px;
object-fit: cover;
`;

const AddImageLabel = styled.label`
width: 120px;
height: 120px;
background-color: ${theme.color.b4};
display: flex;
justify-content: center;
align-items: center;
font-size: 32px;
font-weight: 100;
color: ${theme.color.w1};
cursor: pointer;
`;

const AddImageInput = styled.input`
display: none;
`;

export default AddImages;
Loading

0 comments on commit e18743c

Please sign in to comment.