Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
Tags
- 리덕스
- 그리디
- 리액트 패턴
- react pattern
- revalidatepath
- 자바스크립트
- 코테
- 코어자바스크립트
- React Query
- 리액트
- 프로그래머스
- Next.js
- Form
- 동적계획법
- 토이 프로젝트
- 코딩테스트
- TypeScript
- React
- tailwind
- react router dom
- 토이프로젝트
- Supabase
- JavaScript
- 타입스크립트
- 프론트엔드
- 스택
- tanstack query
- reduxtoolkit
- styled component
- 리액트 라우터 돔
Archives
- Today
- Total
느려도 한걸음씩
Pokemon Card Flip - 11-2.포켓몬 도감 기능 만들기(도감 ui 제작) 본문
토이프로젝트/Pokemon Card Flip
Pokemon Card Flip - 11-2.포켓몬 도감 기능 만들기(도감 ui 제작)
hoj0806 2025. 3. 26. 04:17저번 포스트에서 불러온 데이터를 이용해 포켓몬의 상세 정보를 표시해주는 도감을 만들어보자
import { useAppSelector } from "../hooks/useAppSelector";
import { pokemons } from "../slice/pokemonSlice";
import { motion } from "framer-motion";
const PokedexList: React.FC<{
setPokedexDetailPopupOpen: React.Dispatch<React.SetStateAction<boolean>>;
setSelectPokemon: React.Dispatch<React.SetStateAction<string>>;
}> = ({ setPokedexDetailPopupOpen, setSelectPokemon }) => {
const pokemonData = useAppSelector(pokemons);
const handleClickPokeDexListItem = (pokemonName: string) => {
setPokedexDetailPopupOpen(true);
setSelectPokemon(pokemonName);
};
return (
<ol className='bg-white h-full rounded-xl overflow-y-auto grid grid-cols-5 place-items-center gap-7 py-3'>
{pokemonData.map((data) => {
return (
<div
className='cursor-pointer'
onClick={() => handleClickPokeDexListItem(data.pokemonName)}
key={data.id}
>
<li className='rounded-full border border-black w-[100px] h-[100px]'>
<motion.img
src={data.imageUrl}
alt={data.pokemonName}
whileHover={{ scale: 1.1 }}
/>
</li>
<p>{data.pokemonName}</p>
</div>
);
})}
</ol>
);
};
export default PokedexList;
pokemonData는 리덕스에 저장된 api 데이터이다 map 메서드를 이용해 포켓몬 데이터를 렌더링 해준다
motion을 이용해 포켓몬 이미지 hover시 이미지 크기가 커지도록 했다 또한 포켓몬 클릭시 클릭한 포켓몬의 정보를 나타내기 위해
setSelectPokemon 함수를 통해 현재 클릭한 포켓몬의 이름을 저장해준다
이제 포켓몬 선택시 해당 포켓몬의 상세정보를 띄워주는 팝업을 만들어보자
<PokedexListDetail
selectPokemon={selectPokemon}
setPokedexDetailPopupOpen={setPokedexDetailPopupOpen}
/>
포켓몬의 상세 정보를 표시하는 컴포넌트이다 selectPokemon값을 props로 받아준다
const pokemonDatas = useAppSelector(pokemons);
const selectPokemonData = pokemonDatas.find(
(data) => data.pokemonName === selectPokemon
);
PokedexListDetail 컴포넌트 안에서 selectPokemon을 통해 어떤 포켓몬을 선택했는지 알수 있는 변수를 생성한다 pokemonData는 전체 포켓몬 데이터이다 find 메서드를 통해 선택한 포켓몬 이름을 가진 데이터를 selectPokemonData 변수에 저장해준다
import { useState, useRef, useEffect } from "react";
import { useAppSelector } from "../hooks/useAppSelector";
import { pokemons } from "../slice/pokemonSlice";
import { motion } from "framer-motion";
const PokedexListDetail: React.FC<{
selectPokemon: string;
setPokedexDetailPopupOpen: React.Dispatch<React.SetStateAction<boolean>>;
}> = ({ selectPokemon, setPokedexDetailPopupOpen }) => {
const pokemonDatas = useAppSelector(pokemons);
const selectPokemonData = pokemonDatas.find(
(data) => data.pokemonName === selectPokemon
);
console.log(selectPokemonData);
const cardRef = useRef<HTMLDivElement>(null);
const [transform, setTransform] = useState<string>("");
const [glowPosition, setGlowPosition] = useState<string>("50% 50%");
useEffect(() => {
const card = cardRef.current;
if (!card) return;
let bounds: DOMRect;
const handleMouseMove = (e: MouseEvent) => {
if (!bounds) bounds = card.getBoundingClientRect();
const mouseX = e.clientX;
const mouseY = e.clientY;
const leftX = mouseX - bounds.left;
const topY = mouseY - bounds.top;
const center = {
x: leftX - bounds.width / 2,
y: topY - bounds.height / 2,
};
const distance = Math.sqrt(center.x ** 2 + center.y ** 2);
// Apply 3D rotation and scale
setTransform(`
scale3d(1.07, 1.07, 1.07)
rotate3d(
${center.y / 100},
${-center.x / 100},
0,
${Math.log(distance) * 2}deg
)
`);
// Update glow position
setGlowPosition(`
${center.x * 2 + bounds.width / 2}px
${center.y * 2 + bounds.height / 2}px
`);
};
const handleMouseLeave = () => {
setTransform("");
setGlowPosition("50% 50%");
};
card.addEventListener("mousemove", handleMouseMove); // 카드에만 mousemove 이벤트 등록
card.addEventListener("mouseleave", handleMouseLeave);
return () => {
card.removeEventListener("mousemove", handleMouseMove); // 이벤트 제거
card.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
if (!selectPokemonData) return null;
return (
<motion.div
className='fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm bg-black/50'
onClick={() => setPokedexDetailPopupOpen(false)} // 배경 클릭 시 팝업 닫기
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{/* Wrapper with blur effect */}
<div
ref={cardRef}
className='w-[400px] h-[600px] font-bold p-4 text-right text-[#181a1a] rounded-lg shadow-lg relative transition-all duration-300 ease-out cursor-pointer'
style={{
transform,
background: `linear-gradient(
145deg,
#FF3B30 0%, /* 밝은 빨간색 (rgba(255, 59, 48, 1)) */
#F7352B 10%, /* 보간된 색상 */
#EF2F26 20%, /* 보간된 색상 */
#E63228 25%, /* 중간 빨간색 (rgba(230, 50, 40, 1)) */
#DE2C22 30%, /* 보간된 색상 */
#D6281E 40%, /* 보간된 색상 */
#C8281E 50%, /* 어두운 빨간색 (rgba(200, 40, 30, 1)) */
#C0241A 60%, /* 보간된 색상 */
#B82016 70%, /* 보간된 색상 */
#B41E14 75%, /* 더 어두운 빨간색 (rgba(180, 30, 20, 1)) */
#AC1A10 80%, /* 보간된 색상 */
#A4160C 90%, /* 보간된 색상 */
#96140A 100% /* 가장 어두운 빨간색 (rgba(150,
)`,
boxShadow: transform
? "0 20px 30px rgba(0, 0, 0, 0.3), inset 0 0 10px rgba(255, 255, 255, 0.5)" // hover 시 그림자 강조
: "0 10px 20px rgba(0, 0, 0, 0.2), inset 0 0 5px rgba(255, 255, 255, 0.3)", // 기본 그림자
}}
onClick={(e) => e.stopPropagation()} // 카드 클릭 시 이벤트 전파 방지
>
{/* Glow effect */}
<div
className='absolute inset-0 w-full h-full pointer-events-none rounded-lg'
style={{
backgroundImage:
glowPosition !== "50% 50%" // glowPosition이 초기값이 아닐 때만 glow 효과 적용
? `radial-gradient(
circle at ${glowPosition},
rgba(255, 255, 255, 0.33),
rgba(0, 0, 0, 0.06)
)`
: "none", // 초기값일 때는 glow 효과 없음
}}
/>
{/* Pokemon Image */}
<div className='w-[90%] h-[80%] flex justify-center items-center overflow-hidden mx-auto mt-4'>
<img
src={selectPokemonData.imageUrl}
alt={selectPokemonData.pokemonName}
className='w-full h-full object-contain'
/>
</div>
{/* Pokemon Name with Glow Effect */}
<div className='absolute bottom-4 right-4 text-xl font-bold'>
{selectPokemonData.pokemonName}
</div>
</div>
</motion.div>
);
};
export default PokedexListDetail;
1. 3D 카드 호버 효과 (useEffect + mousemove 이벤트)
- 마우스를 카드 위에 올리면 입체적으로 기울어지고 확대
- mousemove 이벤트를 이용해 마우스 위치에 따라 회전(rotate3d)과 확대(scale3d) 효과를 적용
- 마우스가 카드에서 벗어나면 원래 상태로 돌아감
2. 카드 빛나는 효과 (box-shadow)
- 기본적으로 부드러운 그림자가 들어가 있고,
- 마우스를 올리면 내부에서 강한 빛(glow) 효과가 나타남
boxShadow: transform ?
"0 20px 30px rgba(0, 0, 0, 0.3), inset 0 0 10px rgba(255, 255, 255, 0.5)" :
"0 10px 20px rgba(0, 0, 0, 0.2), inset 0 0 5px rgba(255, 255, 255, 0.3)";
3. 마우스 위치 기반 빛 반사 (glow effect)
- glowPosition을 이용해 마우스 위치를 계산하고,
- 해당 지점을 중심으로 빛나는 효과(radial-gradient)를 추가
backgroundImage: glowPosition !== "50% 50%" ? `radial-gradient(circle at ${glowPosition}, rgba(255, 255, 255, 0.33), rgba(0, 0, 0, 0.06))`
: "none";
4. 팝업 효과 (motion.div - Framer Motion)
- motion.div로 부드러운 나타남/사라짐 애니메이션을 추가.→ 팝업이 뜰 때 자연스럽게 fade-in
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
카드의 배경화면은 포켓몬의 타입을 받아 동적으로 타입과 어울리는 색상을 linear-gradinet로 지정해줬다
// 매개변수로 타입을 받아 gradients 객체의 key값과 일치하는 linear-gradient 반환
const getGradientByType = (type: string): string => {
const gradients: { [key: string]: string } = {
불꽃: `linear-gradient(145deg, #FF3B30 0%, #D6281E 50%, #96140A 100%)`,
물: `linear-gradient(145deg, #30A2FF 0%, #1E66D6 50%, #0A3696 100%)`,
풀: `linear-gradient(145deg, #3BFF30 0%, #1ED628 50%, #0A9614 100%)`,
전기: `linear-gradient(145deg, #FFD700 0%, #E6C200 50%, #B89C00 100%)`,
얼음: `linear-gradient(145deg, #A0EFFF 0%, #50C8E6 50%, #0A96B8 100%)`,
격투: `linear-gradient(145deg, #D63434 0%, #A02525 50%, #701818 100%)`,
독: `linear-gradient(145deg, #B050C8 0%, #7A3696 50%, #561078 100%)`,
땅: `linear-gradient(145deg, #E0C080 0%, #C8A050 50%, #A08030 100%)`,
비행: `linear-gradient(145deg, #A0C8FF 0%, #70A0E6 50%, #5078C8 100%)`,
에스퍼: `linear-gradient(145deg, #FF70A0 0%, #E65078 50%, #C83050 100%)`,
벌레: `linear-gradient(145deg, #A0C850 0%, #789C30 50%, #506A20 100%)`,
바위: `linear-gradient(145deg, #C8B878 0%, #A09050 50%, #786830 100%)`,
고스트: `linear-gradient(145deg, #705898 0%, #503878 50%, #301850 100%)`,
드래곤: `linear-gradient(145deg, #7860E0 0%, #5038C8 50%, #3010A0 100%)`,
악: `linear-gradient(145deg, #505050 0%, #282828 50%, #101010 100%)`,
강철: `linear-gradient(145deg, #B8B8D0 0%, #9090B0 50%, #70708A 100%)`,
페어리: `linear-gradient(145deg, #FFB0FF 0%, #E690E6 50%, #C870C8 100%)`,
};
return (
gradients[type] ||
`linear-gradient(145deg, #A8A878 0%, #787860 50%, #585848 100%)`
); // 기본값(노멀 타입 )
};
const primaryType = selectPokemonData!.types[0]; // 첫 번째 타입 기준
const backgroundGradient = getGradientByType(primaryType);
'토이프로젝트 > Pokemon Card Flip' 카테고리의 다른 글
Pokemon Card Flip - 13. 웹사이트 완성 및 배포 (0) | 2025.03.31 |
---|---|
Pokemon Card Flip - 12. 점수기능 제작 (0) | 2025.03.30 |
Pokemon Card Flip - 11-1.포켓몬 도감 기능 만들기(데이터 패칭) (0) | 2025.03.26 |
Pokemon Card Flip - 10.framer-motion으로 타이머 바 만들기 (0) | 2025.03.20 |
Pokemon Card Flip - 9.게임 보드 만들기(2) (0) | 2025.03.20 |