지금 캠핑 상세 화면의 이미지만 먼저 작업중에 있다. 아직 이미지 외의 다른 데이터는 작업 x
이미지를 누르면 전체 사진을 보여주려 한다. 우선 모달창만 구현한 화면.
component/Modal
interface IPropsModal {
isShowing: boolean;
hide: () => void;
message: string;
}
export function Modal({ isShowing, hide, message }: IPropsModal) {
return isShowing
? ReactDOM.createPortal(
<Wrap onClick={hide}>
<ModalInner
className="modal"
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}}
>
<div>
<AlertIcon />
<p>{message}</p>
</div>
<Button onClick={hide}>닫기</Button>
</ModalInner>
</Wrap>,
document.body,
)
: null;
}
모달창을 따로 만들 필요는 없을 것 같아서 전에 alert창 구현으로 만들어놓은 Modal을 이용하려 한다.
const ModalInner = styled.div<{ customStyle?: React.CSSProperties }>`
padding: 3rem 4rem 2rem;
min-width: 40rem;
min-height: 20rem;
margin: auto;
background-color: white;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
border-radius: 2rem;
text-align: center;
box-shadow: 0 2px 30px 0 rgba(0, 0, 0, 0.2);
${({ customStyle }) => customStyle && { ...customStyle }}
@media ${responsive.mobile} {
width: calc(100% - 3.2rem);
}
p {
margin-top: 1rem;
}
`;
const AlertIcon = styled(IoAlertCircleOutline)`
width: 4.4rem;
height: 4.4rem;
stroke: #67794a;
fill: #67794a;
`;
interface IPropsModal {
isShowing: boolean;
hide: () => void;
message?: string;
children?: ReactNode;
customStyle?: React.CSSProperties;
}
export function Modal({
isShowing,
hide,
message,
children,
customStyle,
}: IPropsModal) {
return isShowing
? ReactDOM.createPortal(
<Wrap onClick={hide}>
<ModalInner
customStyle={customStyle}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}}
>
{message ? (
<div>
<AlertIcon />
<p>{message}</p>
</div>
) : (
children
)}
<Button onClick={hide}>닫기</Button>
</ModalInner>
</Wrap>,
document.body,
)
: null;
}
alert일 때는 메세지만 들어가면 되니까 alert창과 modal창일 때로 구분을 해준다.
모달창일 때는 닫기 버튼을 우측 상단으로 바꿀 것 같은데 일단 확인용으로 진행.
ModalInner는 모달창마다 디자인이 다를 수도 있으니 스타일을 다르게 주는 것이 필요했다.
스타일을 넘겨주려면 React.CSSProperties 타입을 사용하면 된다고 한다.
components/modal/imageDetailModal.tsx
import styled from "@emotion/styled";
import { Modal } from "./";
const customStyle = {
backgroundColor: "blue",
};
const Wrap = styled.div`
background: red;
width: 100%;
height: 100%;
`;
interface IPropsModal {
isShowing: boolean;
hide: () => void;
}
export default function ImageDetailModal({ isShowing, hide }: IPropsModal) {
return (
<Modal isShowing={isShowing} hide={hide} customStyle={customStyle}>
<Wrap>이미지 모달이다 모달이야 모달요</Wrap>
</Modal>
);
}
모달창 안에 들어갈 내용을 넣어준다.
customStyle로 스타일도 넣어줌.
components/campingDetail/imageDetail/index.tsx
interface IImageList {
serialNum: number;
imageUrl: string;
}
interface IApiResponse {
response: {
body: {
items: {
item: IImageList[];
};
};
};
}
interface IPropsImageDetail {
onClick?: () => void;
}
export default function ImageDetail({ onClick }: IPropsImageDetail) {
const [imageUrl, setImageUrl] = useState<IImageList[]>([]);
const router = useRouter();
const contentId = Number(router.query.contentId);
const SERVICE_KEY = process.env.NEXT_PUBLIC_SERVICE_KEY;
useEffect(() => {
async function fetchData(): Promise<void> {
if (!contentId) return;
try {
const response = await axios.get<IApiResponse>(
`http://apis.data.go.kr/B551011/GoCamping/imageList?serviceKey=${SERVICE_KEY}&MobileOS=ETC&MobileApp=dayCamping&contentId=${contentId}&numOfRows=30&_type=json`,
);
setImageUrl(response.data.response?.body?.items.item);
} catch (e) {
console.error(e);
}
}
void fetchData();
}, [contentId, SERVICE_KEY]);
const campingImage = imageUrl.slice(1, 6); // 1번 이미지는 정보 이미지 같아서 우선 제외
return (
<>
<ImgWrap>
<div className="left">
<div onClick={onClick} className="img">
<img src={campingImage[0]?.imageUrl} alt="캠핑이미지" />
</div>
</div>
<div className="right">
<div>
<div onClick={onClick} className="img">
<img src={campingImage[1]?.imageUrl} alt="캠핑이미지" />
</div>
<div onClick={onClick} className="img">
<img src={campingImage[2]?.imageUrl} alt="캠핑이미지" />
</div>
</div>
<div>
<div onClick={onClick} className="img">
<img src={campingImage[3]?.imageUrl} alt="캠핑이미지" />
</div>
<div onClick={onClick} className="img">
<img src={campingImage[4]?.imageUrl} alt="캠핑이미지" />
</div>
</div>
</div>
<button onClick={onClick} className="all_view">
전체 사진 보기
</button>
</ImgWrap>
</>
);
}
캠핑 카드 클릭시 해당하는 캠핑장의 contentId를 쿼리로 받아와서 contentId로 선언해준다(숫자로 변환)
이미지 정보 목록 api로 가져왔다.
캠핑장 상세페이지엔 이미지 5개만 필요해서 slice로 5개를 잘랐는데 첫번째 이미지는 정보 이미지 같아서 우선 제외했다.
근데 다 그런건 아니고 캠핑장마다 달라서 어떤 기준인지는 확인이 필요해보인다.
이미지 개수가 10개로 되어있고 총 개수는 캠핑장마다 다르다.
파라미터엔 없지만 기본목록처럼 numOfRows를 추가해서 받아올 수 있지 않을까 해서 넣어봤더니 된다.
contentId 뒤에 넣어주었다.
타입은 json 타입으로, 이미지는 최대 30개 정도만,
이미지와 버튼 온클릭 이벤트를 만들어서 넘겨주었다.
캠핑장 검색 화면
components/campingList/campingCardList/index.tsx
export default function CampingCardList({ className }: IPropsList) {
....
const onClickCard = (item: ICampingList) => {
const { contentId } = item;
void router.push(`/campingDetail?contentId=${contentId}`);
};
...
return (
<>
<Wrap>
{loading ? (
<Loading />
) : list.length > 0 ? (
<>
<CampingCard className="card" list={list} onClick={onClickCard} />
{pageCount > 0 && (
<Pagination
totalItems={totalCount}
onClick={onClickPage}
currentPage={currentPage}
pageCount={5}
itemCountPerPage={PER_PAGE}
/>
)}
</>
) : (
<NoData />
)}
</Wrap>
</>
);
'언어 > Next.js' 카테고리의 다른 글
[Next 고캠핑] 파이어베이스 중복 오류, 회원가입 (0) | 2024.08.12 |
---|---|
[Next 고캠핑] 뒤로가기 클릭시 모달창 닫기 (0) | 2024.08.12 |
[Next 고캠핑] 커스텀훅으로 alert창 만들기 (0) | 2024.07.27 |
[Next 고캠핑] 드롭다운 선택으로 검색을 해보자 (타입스크립트) (0) | 2024.07.25 |
[Next 고캠핑] 페이지네이션 커스텀하기 (1) | 2024.07.24 |