Skip to content

Commit

Permalink
Feat: 이미지 첨부 기능 구현 및 리스트 생성 제출 API 연동 (#17)
Browse files Browse the repository at this point in the history
* Chore: 불필요 코드 및 파일 삭제, 타입 수정

* Refactor: 프리뷰 컴포넌트 image, link 용도별 타입 분리 및 수정

* Feat: 이미지 업로드 버튼에 파일첨부 기능 추가

* Feat: 헤더 완료버튼에 submit 함수 연결 및 데이터 api별 분리

* Fix: 프리뷰 이미지 초기 렌더링시 src 없음 오류 해결

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

* Fix: Merge 충돌 해결

* Fix: 중복 파일 삭제

* Chore: 주석 및 불필요코드 삭제

* Feat: 리액트 쿼리 사용하여 리스트 생성 전 과정 실행 기능 추가

* Fix: 프리뷰에 링크 도메인 노출 오류로 기능 제거

* Refactor: 헤더 다음 버튼 비활성화 기능 추가 및 카테고리 기본값 설정 변경

* Feat: 헤더 컴포넌트화 (유진님의 리스트 헤더 가져오기)

* HOTFIX: 빌드 오류 해결 위한 미사용 코드 제거

* Fix: useEffect 디펜던시 리스트 수정
  • Loading branch information
seoyoung-min authored Feb 5, 2024
1 parent c3b990f commit 88ced21
Show file tree
Hide file tree
Showing 21 changed files with 741 additions and 146 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,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);

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);
}
};
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';

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} />

<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

0 comments on commit 88ced21

Please sign in to comment.