Skip to content

Commit

Permalink
feat: 루틴 사진 인증 로직 (#142)
Browse files Browse the repository at this point in the history
* feat: room detail data mock API 생성

* feat: room detail data type 추가

* feat: API 데이터 적용

* style: 안쓰는 prop 제거

* chore: type 수정

* chore: page naming 변경

* feat: memeber rank 컴포넌트 추가

* chore: tawilwind class 적용

* chore: API 컨벤션 수정

* chore: page 컴포넌트 naming 수정

* chore: component prop 수정

* feat: API 초기값 설정

* feat: certificationImage 타입 수정

* chore: app-container 스타일 적용

* feat: CertificationBottomSheet 컴포넌트 분리

* style: ImageList 컴포넌트 제거

* chore: CertifiactionImage 타입 수정

* feat: Image Input 이미지 업로드 및 미리보기 구현

* fix: 데이터 관련 type Error 해결

* design: navbar 스타일 수정

* chore: 변수, 타입, 예시 데이터 수정

* feat: bottomSheet 벗어날 시에 form 에러 초기화

* chore: 컴포넌트 export

* fix: PR 리뷰 적용
  • Loading branch information
nayeon-hub authored Nov 15, 2023
1 parent 594af5d commit edf42a6
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 51 deletions.
117 changes: 117 additions & 0 deletions src/RoomDetail/components/CertificationBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { MouseEvent } from 'react';
import { useFormContext } from 'react-hook-form';
import { FormCertificationImage } from '../types/type';
import ImageInput from './ImageInput';
import { BottomSheet } from '@/shared/BottomSheet';
import { BottomSheetProps } from '@/shared/BottomSheet/components/BottomSheet';

const routine = [
{
routineId: 5,
content: '물 마시기'
},
{
routineId: 9,
content: '아침 먹기'
}
];
const certificationImage = [
{
routineId: 5,
// image: 'https://picsum.photos/200'
image: null
},
{
routineId: 9,
// image: 'https://picsum.photos/200'
image: null
}
];

interface CertificationBottomSheetProps {
bottomSheetProps: BottomSheetProps;
close: () => void;
}

const CertificationBottomSheet = ({
bottomSheetProps
}: CertificationBottomSheetProps) => {
const { watch, handleSubmit } = useFormContext<FormCertificationImage[]>();

const handleFormSubmit = async (data: FormCertificationImage[]) => {
const formData = new FormData();

for (const [key, value] of Object.entries(data)) {
if (value.file) {
formData.append(`${routine[Number(key)].routineId}`, value.file[0]);
}
}
// TODO : Form Data 전송
};

const handleEditButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const files = watch();
const formData = new FormData();

for (const [key, value] of Object.entries(files)) {
if (value.file && value.file.length > 0) {
formData.append(`${routine[Number(key)].routineId}`, value.file[0]);
}
}
// TODO : Form Data 전송
};

return (
<BottomSheet
{...bottomSheetProps}
className="bg-light-main p-0 dark:bg-dark-main"
>
<div className="mx-8 mb-[1.88rem] mt-[1.71rem] font-IMHyemin-bold text-black dark:text-white">
<h1 className="mb-[1.27rem] font-IMHyemin-bold">
모든 칸을 채워주세요
</h1>
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="mb-8 grid grid-cols-2 gap-x-3 gap-y-[1.34rem] rounded-2xl text-black dark:text-white"
id="certificationForm"
>
{routine.map(({ routineId, content }, idx) => {
return (
<ImageInput
key={routineId}
content={content}
image={certificationImage[idx].image}
idx={idx}
/>
);
})}
</form>
<span className="mb-[1rem] block font-IMHyemin-bold text-xs text-dark-gray">
다른 새들이 알아볼 수 있게 찍어주세요!
</span>
{certificationImage.filter((el) => el.image).length ===
routine.length ? (
<button
type="button"
className="btn dark:btn-dark-point btn-light-point w-full"
onClick={handleEditButtonClick}
form="certificationForm"
>
수정
</button>
) : (
<button
className="btn dark:btn-dark-point btn-light-point w-full"
type="submit"
form="certificationForm"
>
인증!
</button>
)}
</div>
</BottomSheet>
);
};

export default CertificationBottomSheet;
84 changes: 84 additions & 0 deletions src/RoomDetail/components/ImageInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import clsx from 'clsx';
import { FormCertificationImage } from '../types/type';
import { Icon } from '@/shared/Icon';

interface ImageInputProps {
content: string;
image: string | null;
idx: number;
}

const ImageInput = ({ content, image, idx }: ImageInputProps) => {
const {
register,
formState: { errors },
clearErrors
} = useFormContext<FormCertificationImage[]>();
const [imgSrc, setImgSrc] = useState(image);

const saveFileImage = (fileBlob: File) => {
const fileUrl = URL.createObjectURL(fileBlob);
setImgSrc(fileUrl);
};

return (
<div>
<div
className={clsx(
'relative mb-1 h-0 w-full overflow-hidden rounded-2xl border border-dark-gray pb-[100%] shadow-[0_1px_4px_0px_rgba(0,0,0,0.2)]',
{
'border-danger': errors[idx]?.file?.message
}
)}
>
<input
type="file"
id={content}
{...register(`${idx}.file`, {
required: '이미지를 넣어주세요'
})}
className="absolute left-0 top-0 h-full w-full before:block before:h-full before:w-full before:bg-white before:content-[''] after:absolute"
onChange={(e) => {
if (e.target.files) {
saveFileImage(e.target.files[0]);
clearErrors(`${idx}.file`);
}
}}
/>
<label
htmlFor={content}
className="absolute block h-full w-full"
>
{imgSrc ? (
<div
className="h-full w-full bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url(${imgSrc})` }}
/>
) : (
<div>
<Icon
icon="FaPlus"
size="4xl"
className={clsx(
'absolute left-[50%] top-[50%] block translate-x-[-50%] translate-y-[-50%] text-dark-gray'
)}
/>
</div>
)}
</label>
</div>
<div>
{errors[idx]?.file?.message && (
<span className="block font-IMHyemin-bold text-sm text-danger">
<>{errors[idx]?.file?.message}</>
</span>
)}
<span className="block font-IMHyemin-bold text-sm">{content}</span>
</div>
</div>
);
};

export default ImageInput;
20 changes: 0 additions & 20 deletions src/RoomDetail/components/ImageList.tsx

This file was deleted.

57 changes: 36 additions & 21 deletions src/RoomDetail/components/RoomRoutine.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
import ImageList from './ImageList';
import { BottomSheet, useBottomSheet } from '@/shared/BottomSheet';
import { FormProvider, useForm } from 'react-hook-form';
import { FormCertificationImage } from '../types/type';
import CertificationBottomSheet from './CertificationBottomSheet';
import { useBottomSheet } from '@/shared/BottomSheet';
import { RoutineList, RoutineItem } from '@/shared/RoutineList';

const certificationImage = [
{
routineId: 5,
// image: 'https://picsum.photos/200'
image: null
},
{
routineId: 9,
// image: 'https://picsum.photos/200'
image: null
}
];

interface RoomRoutineProps {
routines: { routineId: number; content: string }[];
}

const RoomRoutine = ({ routines }: RoomRoutineProps) => {
const { bottomSheetProps, toggle, close } = useBottomSheet();
const form = useForm<FormCertificationImage[]>({
mode: 'onSubmit',
defaultValues: certificationImage.map(() => ({
file: null
}))
});

const handleToggle = () => {
form.clearErrors();
toggle();
};

return (
<>
{
<BottomSheet {...bottomSheetProps}>
<div className="mx-[1.81rem] mb-[1.88rem] mt-[1.69rem] text-white">
<h1 className="text-white">모든 칸을 채워주세요</h1>
<ImageList />
<span className="mb-[3.44rem] block text-xs">
다른 새들이 알아볼 수 있게 찍어주세요!
</span>
<button
className="btn dark:btn-dark-point btn-light-point w-full"
onClick={close}
>
인증!
</button>
</div>
</BottomSheet>
}
<FormProvider {...form}>
<CertificationBottomSheet
bottomSheetProps={bottomSheetProps}
close={close}
/>
</FormProvider>
<div className="mb-[0.88rem] flex justify-between text-base">
<h4 className="text-black dark:text-white">개인 인증</h4>
<span className="text-dark-gray">아직이예여</span>
Expand All @@ -44,8 +59,8 @@ const RoomRoutine = ({ routines }: RoomRoutineProps) => {
))}
</RoutineList>
<button
className="btn btn-light-point dark:btn-dark-point w-full rounded-s-lg text-base"
onClick={toggle}
className="btn btn-light-point dark:btn-dark-point w-full rounded-lg text-base"
onClick={handleToggle}
>
인증 하기!
</button>
Expand Down
3 changes: 3 additions & 0 deletions src/RoomDetail/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as RoomNotice } from './components/RoomNotice';
export { default as RoomInfo } from './components/RoomInfo';
export { default as RoomWorkspace } from './components/RoomWorkspace';
3 changes: 3 additions & 0 deletions src/RoomDetail/types/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface FormCertificationImage {
file: null | FileList;
}
2 changes: 1 addition & 1 deletion src/core/types/Image.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type CertificationImage = {
routineId: number;
image: string;
image: string | null;
};
9 changes: 2 additions & 7 deletions src/pages/RoomDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { Link } from 'react-router-dom';

import { useQuery } from '@tanstack/react-query';
import { roomOptions } from '@/core/api/options';

import RoomInfo from '@/RoomDetail/components/RoomInfo';
import RoomNotice from '@/RoomDetail/components/RoomNotice';
import RoomWorkspace from '@/RoomDetail/components/RoomWorkspace';

import { RoomInfo, RoomNotice, RoomWorkspace } from '@/RoomDetail';
import { Header } from '@/shared/Header';
import { Icon } from '@/shared/Icon';

Expand All @@ -22,7 +17,7 @@ const RoomDetailPage = () => {
const { title, announcement } = roomDetailData;

return (
<div className="relative">
<div className="relative h-full overflow-y-scroll">
<Header
title={title}
className="absolute z-[1] text-white"
Expand Down
2 changes: 1 addition & 1 deletion src/shared/Navbar/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface NavbarProps {

const Navbar = ({ currentPath = '' }: NavbarProps) => {
return (
<nav className="flex h-16 w-full overflow-hidden rounded-t-lg bg-light-sub text-2xl text-black shadow-nav dark:bg-dark-sub dark:text-white">
<nav className="z-navbar flex h-16 w-full overflow-hidden rounded-t-lg bg-light-sub text-2xl text-black shadow-nav dark:bg-dark-sub dark:text-white">
{NavbarOptions.map(({ icon, route, activeRoutes }) => (
<Link
to={route}
Expand Down
3 changes: 2 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export default {
},
zIndex: {
bottomSheet: '100',
toast: '110'
toast: '110',
navbar: '50'
}
}
}
Expand Down

0 comments on commit edf42a6

Please sign in to comment.