Unsplash Photo API 연동

This commit is contained in:
choibk 2025-12-17 21:49:30 +09:00
parent 5481e62a38
commit dd689b97b6
13 changed files with 473 additions and 10 deletions

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 19.92L8.48 13.4C7.71 12.63 7.71 11.37 8.48 10.6L15 4.07999" stroke="#98A2B3" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.91 19.92L15.43 13.4C16.2 12.63 16.2 11.37 15.43 10.6L8.91 4.07999" stroke="#98A2B3" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@ -0,0 +1,65 @@
.footer {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 56px;
padding: 0 16px;
border: 1px solid $color-gray-100;
background-color: $color-white-000;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
gap: 8px;
&__button {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 3px 7px;
color: $color-gray-500;
font-family: 'Public Sans', sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 16px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
&:hover {
background-color: $color-gray-100;
border-radius: 4px;
color: $color-black-900;
}
&.active {
background-color: $color-gray-100;
border-radius: 4px;
color: $color-black-900;
}
&.inactive {
background-color: $color-white-000;
border-radius: 4px;
color: $color-black-900;
}
}
}

View File

@ -0,0 +1,85 @@
// import { useEffect, useState } from 'react'
// import { useRecoilState, useRecoilValue, useRecoilValueLoadable } from 'recoil'
// import { imageData } from '@/recoil/selectors/imageSelector'
// import { pageState } from '@/recoil/atoms/pageState'
import styles from './CommonFooter.module.scss'
// import { searchState } from '@/recoil/atoms/searchState'
import IconImageLeft from '@/assets/icons/icon-arrowLeft.svg'
import IconImageRight from '@/assets/icons/icon-arrowRight.svg'
function CommonFooter() {
// const imgSelector = useRecoilValueLoadable(imageData)
// const search = useRecoilValue(searchState)
// const [page, setPage] = useRecoilState(pageState)
// const [step, setStep] = useState(0)
// useEffect(() => {
// setStep(0)
// }, [search])
// // 페이지 리스트 UI 생성
// const newArr: number[] = new Array()
// for (let i = 1; i <= imgSelector.contents.total_pages; i++) {
// newArr.push(i)
// }
// const length = newArr.length
// const divide = Math.floor(length / 10) + (Math.floor(length % 10) > 0 ? 1 : 0)
// const res = []
// for (let i = 0; i <= divide; i++) {
// // 배열 0부터 n개씩 잘라 새 배열에 넣기
// res.push(newArr.splice(0, 10))
// }
// // ----------------------------------------------------------------------------------------------------
// const moveToPage = (selected: number) => {
// setPage(selected)
// }
// const moveToPrev = () => {
// if (step === 0) return
// else {
// setStep(step - 1)
// setPage(res[step - 1][0])
// }
// }
// const moveToNext = () => {
// if (step < res[step].length - 2) {
// setStep(step + 1)
// setPage(res[step + 1][0])
// } else return
// }
return (
<footer className={styles.footer}>
<div className={styles.pagination}>
<button className={styles.pagination__button} /*onClick={moveToPrev}*/>
<img src={IconImageLeft} alt="IconImageLeft" />
</button>
{/* 변경될 UI 부분 */}
<span>1</span>
{/* {res[step] &&
res[step].map((item: number, index: number) => {
if (item < 11) {
return (
<button className={index === page - 1 ? `${styles.pagination__button} ${styles.active}` : `${styles.pagination__button} ${styles.inactive}`} key={item} onClick={() => moveToPage(item)}>
{item}
</button>
)
} else {
return (
<button className={index === page - 1 - step * 10 ? `${styles.pagination__button} ${styles.active}` : `${styles.pagination__button} ${styles.inactive}`} key={item} onClick={() => moveToPage(item)}>
{item}
</button>
)
}
})} */}
<button className={styles.pagination__button} /*onClick={moveToNext}*/>
<img src={IconImageRight} alt="IconImageRight" />
</button>
</div>
</footer>
)
}
export default CommonFooter

View File

@ -0,0 +1,47 @@
import { useEffect, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styles from './CommonNav.module.scss'
import navJson from './nav.json'
// import { useRecoilState } from 'recoil'
// import { pageState } from '@/recoil/atoms/pageState'
// import { searchState } from '@/recoil/atoms/searchState'
interface Navigation {
index: number
path: string
label: string
searchValue: string
isActive: boolean
}
function CommonNav() {
const location = useLocation()
const [navigation, setNavigation] = useState<Navigation[]>(navJson)
// const [, setPage] = useRecoilState(pageState)
// const [, setSearch] = useRecoilState(searchState)
// useEffect(() => {
// navigation.forEach((nav: Navigation) => {
// nav.isActive = false
// if (nav.path === location.pathname || location.pathname.includes(nav.path)) {
// nav.isActive = true
// setSearch(nav.searchValue)
// setPage(1)
// }
// })
// setNavigation([...navigation])
// }, [location.pathname])
// useState로 선언한 반응성을 가진 데이터를 기반으로 UI를 반복호출해보도록 한다.
const navLinks = navigation.map((item: Navigation) => {
return (
<Link to={item.path} className={item.isActive ? `${styles.navigation__menu} ${styles.active}` : `${styles.navigation__menu} ${styles.inactive}`} key={item.path}>
<span className={styles.navigation__menu__label}>{item.label}</span>
</Link>
)
})
return <nav className={styles.navigation}>{navLinks}</nav>
}
export default CommonNav

View File

@ -0,0 +1,37 @@
.navigation {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 50px;
gap: 32px;
border-bottom: 1px solid $color-white-300;
&__menu {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
text-decoration: none; // 추후에 div 태그를 React Router 속성 Link 태그로 바꿀 것이기 때문입니다.
color: $color-white-700;
font-weight: 500;
&.inactive {
color: $color-white-700;
border-bottom: none;
}
&.active {
color: $color-black-900;
border-bottom: 2px solid $color-white-700;
}
&:hover {
color: $color-black-900;
font-weight: 600;
}
}
}

View File

@ -0,0 +1,86 @@
[
{
"index": 0,
"path": "/search/edit",
"label": "보도/편집 전용",
"searchValue": "edit",
"isActive": false
},
{
"index": 1,
"path": "/search/following",
"label": "팔로잉",
"searchValue": "following",
"isActive": false
},
{
"index": 2,
"path": "/search/photoPlus",
"label": "Unsplash Photo+",
"searchValue": "photo",
"isActive": false
},
{
"index": 3,
"path": "/search/oneColor",
"label": "단색",
"searchValue": "one color",
"isActive": false
},
{
"index": 4,
"path": "/search/3dRender",
"label": "3D 렌더링",
"searchValue": "3d rendering",
"isActive": false
},
{
"index": 5,
"path": "/search/nature",
"label": "자연",
"searchValue": "nature",
"isActive": false
},
{
"index": 6,
"path": "/search/texture",
"label": "텍스쳐 및 패턴",
"searchValue": "texture",
"isActive": false
},
{
"index": 7,
"path": "/search/interior",
"label": "인테리어",
"searchValue": "interior",
"isActive": false
},
{
"index": 8,
"path": "/search/film",
"label": "필름",
"searchValue": "film",
"isActive": false
},
{
"index": 9,
"path": "/search/experimental",
"label": "실험적인",
"searchValue": "experimental",
"isActive": false
},
{
"index": 10,
"path": "/search/travel",
"label": "여행",
"searchValue": "travel",
"isActive": false
},
{
"index": 11,
"path": "/search/sports",
"label": "스포츠",
"searchValue": "sports",
"isActive": false
}
]

View File

@ -1,9 +1,5 @@
import { StrictMode } from 'react'
// import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
createRoot(document.getElementById('root')!).render(<App />)

View File

@ -0,0 +1,22 @@
.card {
width: 260px;
height: 260px;
background-color: $color-gray-200;
border-radius: 6px;
cursor: pointer;
@media (min-width: 1024px) and (max-width: 1440px) {
width: 200px;
height: 200px;
}
&__image {
width: 100%;
height: 100%;
border-radius: 6px;
object-fit: cover;
}
}

View File

@ -0,0 +1,25 @@
import type { CardDTO } from '../types/card'
import styles from './Card.module.scss'
interface Props {
data: CardDTO
handleDialog: (eventValue: boolean) => void
handleSetData: (eventValue: CardDTO) => void
}
function Card({ data/*, handleDialog, handleSetData */}: Props) {
const openDialog = () => {
console.log('함수호출')
// handleDialog(true)
// handleSetData(data)
}
return (
<div className={styles.card} onClick={openDialog}>
<img src={data.urls.small} alt={data.alt_description} className={styles.card__image} />
</div>
)
}
export default Card

View File

@ -1,13 +1,51 @@
import CommonHeader from "@/components/common/header/CommonHeader"
import styles from "./styles/index.module.scss"
import CommonSearchBar from "@/components/common/searchBar/CommonSearchBar"
import CommonNav from "@/components/common/navigation/CommanNav"
import CommonFooter from "@/components/common/footer/CommonFooter"
import Card from "./components/Card"
import styles from "./styles/index.module.scss"
import axios from "axios"
import { useEffect, useState } from "react"
import type { CardDTO } from "./types/card"
function index() {
const [imgUrls, setImgUrls] = useState([])
const getData = async() => {
// Open API 호출
const API_URL = 'https://api.unsplash.com/search/photos'
const API_KEY = 'nloztIOd94-5EH6vAJUU3L66l79z9Dv2KwwbKHShAnY'
const PER_PAGE = 30
const searchValue = 'Korea'
const pageValue = 100
try {
const res = await axios.get(`${API_URL}?query=${searchValue}&client_id=${API_KEY}&page=${pageValue}&per_page=${PER_PAGE}`)
console.log(res)
if(res.status === 200) {
setImgUrls(res.data.results)
}
} catch (error) {
console.log(error)
}
}
const cardList = imgUrls.map((card: CardDTO) => {
return <Card data={card} key={card.id} />
})
useEffect(()=> {
getData()
}, [])
return (
<div className={styles.page}>
{/*공통 헤더 UI 부분*/}
<CommonHeader />
{/*공통 네비게이션 UI 부분*/}
<CommonNav />
<div className={styles.page__contents}>
<div className={styles.page__contents__introBox}>
<div className={styles.wrapper}>
@ -17,12 +55,15 @@ function index() {
.
</span>
{/*검색창 UI 부분*/}
<CommonSearchBar />
<CommonSearchBar />
</div>
</div>
<div className={styles.page__contents__imageBox}></div>
<div className={styles.page__contents__imageBox}>
{cardList}
</div>
</div>
{/* 공통 푸터 UI 부분 */}
<CommonFooter />
</div>
)
}

View File

@ -0,0 +1,53 @@
export interface CardDTO {
alt_description: string
blur_hash: string
breadcrumbs: []
color: string
created_at: string
current_user_collections: []
description: string
height: number
id: string
liked_by_user: boolean
likes: number
links: Link
promoted_at?: string
slug: string
sponsorship?: string
tags: Tag[]
topic_submissions: any
updated_at: string
urls: Url
user: any
width: number
}
interface Link {
download: string
download_location: string
html: string
self: string
}
export interface Tag {
source: {
ancestry: any
cover_photo: any
description: string
meta_description: string
meta_title: string
subtitle: string
title: string
}
title: string
type: string
}
interface Url {
full: string
raw: string
regular: string
small: string
small_s3: string
thumb: string
}

View File

@ -1,3 +1,3 @@
{
"extends": "./tsconfig.app.json"
"extends": "./tsconfig.app.json"
}