Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 이미지 첨부 기능 구현 및 리스트 생성 제출 API 연동 #17

Merged
merged 17 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6469078
Chore: 불필요 코드 및 파일 삭제, 타입 수정
seoyoung-min Feb 3, 2024
446a431
Refactor: 프리뷰 컴포넌트 image, link 용도별 타입 분리 및 수정
seoyoung-min Feb 4, 2024
bee77d4
Feat: 이미지 업로드 버튼에 파일첨부 기능 추가
seoyoung-min Feb 4, 2024
272bc40
Feat: 헤더 완료버튼에 submit 함수 연결 및 데이터 api별 분리
seoyoung-min Feb 4, 2024
e128a37
Fix: 프리뷰 이미지 초기 렌더링시 src 없음 오류 해결
seoyoung-min Feb 4, 2024
6b18093
feat: 이미지 업로드 api 연동
seoyoung-min Feb 4, 2024
da8917b
Fix: Merge 충돌 해결
seoyoung-min Feb 4, 2024
209eb15
Fix: 중복 파일 삭제
seoyoung-min Feb 4, 2024
756ed34
Chore: 주석 및 불필요코드 삭제
seoyoung-min Feb 4, 2024
261dd45
Feat: 리액트 쿼리 사용하여 리스트 생성 전 과정 실행 기능 추가
seoyoung-min Feb 4, 2024
9fc4009
Fix: 프리뷰에 링크 도메인 노출 오류로 기능 제거
seoyoung-min Feb 4, 2024
c344b91
Refactor: 헤더 다음 버튼 비활성화 기능 추가 및 카테고리 기본값 설정 변경
seoyoung-min Feb 4, 2024
6289709
Feat: 헤더 컴포넌트화 (유진님의 리스트 헤더 가져오기)
seoyoung-min Feb 4, 2024
4c369e4
Merge branch 'dev' into feature/create-item-image
seoyoung-min Feb 4, 2024
0b92fab
HOTFIX: 빌드 오류 해결 위한 미사용 코드 제거
seoyoung-min Feb 4, 2024
2ec9815
Fix: useEffect 디펜던시 리스트 수정
seoyoung-min Feb 4, 2024
a2eb657
Merge branch 'dev' into feature/create-item-image
seoyoung-min Feb 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
"zustand": "^4.4.7"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@hookform/devtools": "^4.3.1",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
Expand Down
4 changes: 2 additions & 2 deletions src/app/_api/list/createList.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { ListCreateType } from '@/lib/types/listType';
import { ListCreateType, ListIdType } from '@/lib/types/listType';

export const createList = async (data: ListCreateType) => {
const response = await axiosInstance.post<ListCreateType>('/lists', data);
const response = await axiosInstance.post<ListIdType>('/lists', data);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Eugene-A-01 🖊️ 제네릭으로 response로 받을 타입으로 넣어야 해서 수정했습니다!


return response.data;
};
31 changes: 31 additions & 0 deletions src/app/_api/list/uploadItemImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { ItemImagesType, PresignedUrlListType } from '@/lib/types/listType';
import axios from 'axios';

interface uploadItemImagesProps {
listId: number;
imageData: ItemImagesType;
imageFileList: File[];
}

export const uploadItemImages = async ({ listId, imageData, imageFileList }: uploadItemImagesProps) => {
imageData.listId = listId;

//PresignedUrl 생성 요청
const response = await axiosInstance.post<PresignedUrlListType>('/lists/upload-url', imageData);

//PresignedUrl에 이미지 업로드
for (let i = 0; i < response.data.length; i++) {
const result = await axios.put(response.data[i].presignedUrl, imageFileList[i], {
headers: {
'Content-Type': imageFileList[i]?.type,
},
});
if (result.status !== 200) return;
}

//서버에 성공 완료 알림
if (imageFileList.length !== 0) {
await axiosInstance.post('/lists/upload-complete', imageData);
}
};
Comment on lines +1 to +31
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👩‍💻리스트 생성 성공 후 리스트 아이템 이미지를 업로드하는 과정입니다!

queryFn, MutationFn 모두 파라미터 1개만 전달할 수 있는 것 같아요!
여러개를 받아야해서 객체 타입으로 받았습니다!

37 changes: 0 additions & 37 deletions src/app/create/_components/CreateItem.css.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,5 @@
import { style } from '@vanilla-extract/css';

export const header = style({
width: '100%',
height: '90px',
paddingLeft: '20px',
paddingRight: '20px',

position: 'sticky',
top: '0',
left: '0',
zIndex: '10',

display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',

backgroundColor: '#fff',

borderBottom: '1px solid rgba(0, 0, 0, 0.10)',
});

export const headerTitle = style({
fontSize: '2rem',
});

export const headerNextButton = style({
fontSize: '1.6rem',
backgroundColor: 'transparent',
});

export const headerNextButtonDisabled = style([
headerNextButton,
{
color: '#AFB1B6', //활성화 검정, 아닐때는 회색
},
]);

export const article = style({
padding: '16px 20px 30px',
});
Expand Down
20 changes: 4 additions & 16 deletions src/app/create/_components/CreateItem.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
import { useFormContext } from 'react-hook-form';

import BackIcon from '/public/icons/back.svg';
import Header from './item/Header';
import Items from './item/Items';
import * as styles from './CreateItem.css';

interface CreateItemProps {
onBackClick: () => void;
onSubmit: () => void;
onSubmitClick: () => void;
}

export default function CreateItem({ onBackClick, onSubmit }: CreateItemProps) {
export default function CreateItem({ onBackClick, onSubmitClick }: CreateItemProps) {
const {
formState: { isValid },
} = useFormContext();

return (
<div>
<div className={styles.header}>
<button onClick={onBackClick}>
<BackIcon alt="뒤로가기 버튼" />
</button>
<h1 className={styles.headerTitle}>리스트 생성</h1>
<button
onClick={onSubmit}
className={isValid ? styles.headerNextButton : styles.headerNextButtonDisabled}
disabled={!isValid ? true : false}
>
완료
</button>
</div>
<Header onBackClick={onBackClick} isSubmitActive={isValid} onSubmitClick={onSubmitClick} />
<div className={styles.article}>
<h3 className={styles.label}>
아이템 추가 <span className={styles.required}>*</span>
Expand Down
9 changes: 5 additions & 4 deletions src/app/create/_components/CreateList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { UserProfileType } from '@/lib/types/userProfileType';
import { getCategories } from '@/app/_api/category/getCategories';
import { getUsers } from '@/app/_api/user/getUsers';
import { listDescriptionRules, listLabelRules, listTitleRules } from '@/lib/constants/formInputValidationRules';
import { listDescription } from '@/app/[userNickname]/[listId]/_components/ListDetailOuter/ListInformation.css';
// import { listDescription } from '@/app/[userNickname]/[listId]/_components/ListDetailOuter/ListInformation.css';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Eugene-A-01 🖊️ 배포시 미사용 코드로 워닝이 떠서 주석처리했습니다~!


interface CreateListProps {
onNextClick: () => void;
Expand All @@ -39,9 +39,10 @@ function CreateList({ onNextClick }: CreateListProps) {
const [categories, setCategories] = useState<CategoryType[]>([]);
const [users, setUsers] = useState<UserProfileType[]>([]);

const { setValue, control, formState } = useFormContext();
const { isValid } = formState;
const { setValue, control } = useFormContext();
const collaboIDs = useWatch({ control, name: 'collaboratorIds' });
const title = useWatch({ control, name: 'title' });
const category = useWatch({ control, name: 'category' });

const searchParams = useSearchParams();
const isTemplateCreation = searchParams.has('title') && searchParams.has('category');
Expand Down Expand Up @@ -75,7 +76,7 @@ function CreateList({ onNextClick }: CreateListProps) {
return (
<div>
{/* 헤더 */}
<Header isNextActive={isValid} onClickNext={onNextClick} />
<Header isNextActive={title && category} onClickNext={onNextClick} />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Eugene-A-01 🖊️isValid가 아이템 페이지까지 채워져야 true가 돼서 title, category를 watch하여 값이 있을때 true가 되도록 코드를 수정했습니다! 급하게 수정한 것이라 옳은 방식인지는 확신이 없습니다ㅠㅠ!


<div className={styles.body}>
{/* 리스트 제목 */}
Expand Down
36 changes: 36 additions & 0 deletions src/app/create/_components/item/Header.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { style } from '@vanilla-extract/css';

export const header = style({
width: '100%',
height: '90px',
paddingLeft: '20px',
paddingRight: '20px',

position: 'sticky',
top: '0',
left: '0',
zIndex: '10',

display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',

backgroundColor: '#fff',

borderBottom: '1px solid rgba(0, 0, 0, 0.10)',
});

export const headerTitle = style({
fontSize: '2rem',
});

export const headerNextButton = style({
fontSize: '1.6rem',
color: '#AFB1B6',
cursor: 'default',
});

export const headerNextButtonActive = style({
fontSize: '1.6rem',
});
27 changes: 27 additions & 0 deletions src/app/create/_components/item/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import BackIcon from '/public/icons/back.svg';
import * as styles from './Header.css';

interface HeaderProps {
onBackClick: () => void;
isSubmitActive: boolean;
onSubmitClick: () => void;
}

function Header({ onBackClick, isSubmitActive, onSubmitClick }: HeaderProps) {
return (
<div className={styles.header}>
<button onClick={onBackClick}>
<BackIcon alt="뒤로가기 버튼" />
</button>
<h1 className={styles.headerTitle}>리스트 생성</h1>
<button
className={isSubmitActive ? styles.headerNextButtonActive : styles.headerNextButton}
disabled={!isSubmitActive}
onClick={onSubmitClick}
>
완료
</button>
</div>
);
}
export default Header;
9 changes: 9 additions & 0 deletions src/app/create/_components/item/ImageUploader.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { style } from '@vanilla-extract/css';

export const label = style({
cursor: 'pointer',
});

export const input = style({
display: 'none',
});
19 changes: 19 additions & 0 deletions src/app/create/_components/item/ImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactNode } from 'react';
import ImageIcon from '/public/icons/attach_image.svg';
import * as styles from './ImageUploader.css';

interface ImageUploaderProps {
index: number;
children: ReactNode;
}

export default function ImageUploader({ index, children }: ImageUploaderProps) {
return (
<>
<label className={styles.label} htmlFor={`${index}-image`}>
<ImageIcon />
</label>
{children}
</>
);
}
23 changes: 7 additions & 16 deletions src/app/create/_components/item/ItemLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { ReactNode } from 'react';

import DndIcon from '/public/icons/dnd.svg';
import ClearGrayIcon from '/public/icons/clear_x_gray.svg';
import ClearBlackIcon from '/public/icons/clear_x_black.svg';
import ImageIcon from '/public/icons/attach_image.svg';
import Label from '@/components/Label/Label';
import ImageUploader from './ImageUploader';
import * as styles from './ItemLayout.css';

interface ItemLayoutProps {
Expand All @@ -16,6 +15,8 @@ interface ItemLayoutProps {
commentLength: ReactNode;
linkModal: ReactNode;
linkPreview: ReactNode;
imageInput: ReactNode;
imagePreview: ReactNode;
}

export default function ItemLayout({
Expand All @@ -27,6 +28,8 @@ export default function ItemLayout({
commentLength,
linkModal,
linkPreview,
imageInput,
imagePreview,
}: ItemLayoutProps) {
return (
<>
Expand All @@ -50,26 +53,14 @@ export default function ItemLayout({
<div className={styles.toolbar}>
<div className={styles.fileButtons}>
{linkModal}
<button type="button">
<ImageIcon alt="사진 첨부 버튼" />
</button>
<ImageUploader index={index}>{imageInput}</ImageUploader>
</div>
{commentLength}
</div>

<div className={styles.previewContainer}>
{linkPreview}
<div className={styles.previewBox} role="img">
사진칸
<button
className={styles.clearButton}
onClick={() => {
console.log('사진없애기');
}}
>
<ClearBlackIcon alt="사진 삭제 버튼" />
</button>
</div>
{imagePreview}
</div>
</div>
</>
Expand Down
6 changes: 6 additions & 0 deletions src/app/create/_components/item/Items.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const itemsContainer = style({
display: 'flex',
flexDirection: 'column',
gap: '16px',

cursor: 'grab',
});

export const item = style({
Expand Down Expand Up @@ -85,6 +87,10 @@ export const linkInput = style([
},
]);

export const imageInput = style({
display: 'none',
});

export const countLength = style({
//body2
fontSize: '1.5rem',
Expand Down
Loading