๐พ ํดํผํฌ (Happy-Paw) : ๋ฐ๋ ค๋๋ฌผ ํตํฉ ์ปค๋ฎค๋ํฐ ์๋น์ค
ํ๋ก์ ํธ ์๊ฐ
๊ฐ๋ฐ ํ๊ฒฝ
ํ์
ํ๊ฒฝ
์ญํ ๋ถ๋ด
ํ๋ก์ ํธ ๊ตฌํ
ํต์ฌ ์ฝ๋
ํธ๋ฌ๋ธ ์ํ
๐ฑ ๋ฐ๋ ค๋๋ฌผ์ ์ ๋ณด๋ฅผ ๊ณต์ ํ๋ ์ปค๋ฎค๋ํฐ๋ฅผ ๊ตฌ์ถํฉ๋๋ค.
๐๏ธย ์ํ๋ ๋ฌผํ์ ํ๋งคํ๊ฑฐ๋ ๊ตฌ๋งคํ๋ ์๋น์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
๐งค ์ฌ์ฉ์๋ค ๋ผ๋ฆฌ ํ๋ก์ฐ ๊ธฐ๋ฅ์ ํตํด ์๋ก์ ์์์ ๊ณต์ ํ ์ ์์ต๋๋ค.
๐ผ๏ธย ๊ธ๊ณผ ์ฌ์ง์ ํจ๊ป ์
๋ก๋ํ์ฌ ๋ฐ๋ ค ๋๋ฌผ๋ค์ ์ผ์์ ์๋ํ๊ณ ๊ณต์ ํ ์ ์์ต๋๋ค.
๐งก ๋ค๋ฅธ ์ฌ์ฉ์๋ค์ ๊ธ์ ์ข์์๋ฅผ ๋๋ฅด๊ณ ๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.
๐บ๏ธ ์ง๋๊ธฐ๋ฅ์ ํตํด ๋ด์ฃผ๋ณ์ ๋ฐ๋ ค๋๋ฌผ ์นดํ, ๋ณ์, ํธํ
์ ์ฐพ์ ์ ์์ต๋๋ค.
Front-End
Back-End
Co-work & etc
์ ๊ณต๋ API ์ฌ์ฉ
๐พ HappyPaw
โโ ๐ฆ public
โ โโ โญ favicon.ico
โ โโ ๐ index.html
โโ ๐ฆ src
โ โโ ๐ api
โ โโ ๐ assets
โ โ โโ ๐ icon
โ โ โโ ๐ splash
โ โโ ๐ components
โ โ โโ ๐ common
โ โ โ โโ ๐ Button
โ โ โ โโ ๐ Carousel
โ โ โ โโ ๐ Header
โ โ โ โโ ๐ Input
โ โ โ โโ ๐ MainLayout
โ โ โ โโ ๐ Modal
โ โ โ โโ ๐ TabMenu
โ โ โ โโ ๐ Wrapper
โ โ โโ ๐ Follow
โ โ โโ ๐ Post
โ โ โโ ๐ Product
โ โ โโ ๐ Profile
โ โ โโ ๐ Skeleton
โ โโ ๐ context
โ โโ ๐ hooks
โ โโ ๐ pages
โ โ โโ ๐ ChatPage
โ โ โโ ๐ ErrorPage
โ โ โโ ๐ FollowListPage
โ โ โโ ๐ HomePage
โ โ โโ ๐ JoinPage
โ โ โโ ๐ LoginPage
โ โ โโ ๐ MapPage
โ โ โโ ๐ PostPage
โ โ โโ ๐ ProductPage
โ โ โโ ๐ ProfilePage
โ โ โโ ๐ SearchPage
โ โ โโ ๐ SplashPage
โ โโ ๐ routes
โ โโ ๐ styles
| โโ ๐ App.js
| โโ ๐ index.js
ํ์
์ ์ข
๋ฅ ์คย ํ๋ ๋ง ์ ํํ๋ฉฐ,ย ์์ด ์๋ฌธ์ ๋ก ์์ํ๋ค.
์ฃผ์ ๋ ํ๊ธ๋ก ๊ฐ๋จ๋ช
๋ฃํ๊ฒ ์์ฑํ๋ค.
์ฃผ์ ์ ๋ง์ง๋ง ๋ฌธ์๋กย .(๋ง์นจํ)
๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค.
์ฃผ์ ๋ '-๋ค', '-์'๊ณผ ๊ฐ์ ์ด๋ฏธ๋ก ๋๋ด์ง ์๊ณ , ๊ณผ๊ฑฐํ์ ์ฌ์ฉํ์ง ์๋๋ค.
์ฌ๋ฐ๋ฅด์ง ์์ ์ )ย feat: ์นด์นด์ค ๋ก๊ทธ์ธ ์ฐ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค, ํน์ ์ถ๊ฐํจ (#3)
์ณ์ ์ )ย feat: ์นด์นด์ค ๋ก๊ทธ์ธ ์ฐ๋ ๊ธฐ๋ฅ ์ถ๊ฐ (#3)
์ฃผ์ ๋ ์์ค ์ฝ๋๋ฅผ ๋ณด์ง ์๊ณ ๋ ๋ณ๊ฒฝ ์ฌํญ์ด ๋ฌด์์ธ์ง ์ ์ ์๋๋ก ์์ฑํด์ผ ํ๋ค.
์ฌ๋ฐ๋ฅด์ง ์์ ์ )ย design: CSS ์กฐ์ (#4)
์ณ์ ์ )ย design: text box์ layout frame ์ฌ์ด์ left padding ์ถ๊ฐ (#4)
์ปค๋ฐ ๋ฉ์์ง๋ ์ ์ผ์๊ฐ ๋ดค์ ๋ ๋ฌด์์ ํ๋์ง ํ์
ํ ์ ์๊ฒ ์์ธํ ์์ฑํ๋ค.
์ปค๋ฐ ๋ฉ์์ง๋ ์ด๋ป๊ฒ ๋ณด๋จย ๋ฌด์๊ณผ ์ ๋ฅผ ์ค๋ช
ํ๋ค.
ํ ์ปค๋ฐ์๋ ํ ๊ฐ์ง ๊ธฐ๋ฅ๋ง ๋ด๋๋ค.
์ ) ํ๋ฉด ๊ฐ๋ฐ์ ๊ฒฝ์ฐ : ์ปดํฌ๋ํธ ๋จ์๋ก ์ปค๋ฐ
์2 ) ๊ธฐ๋ฅ ๊ฐ๋ฐ์ ๊ฒฝ์ฐ : ๊ฐ ๊ธฐ๋ฅ ๋จ์๋ก ์ปค๋ฐ
- fix: ์ฌ๋ฐ๋ฅด์ง ์์ ๋์์ ๊ณ ์น ๊ฒฝ์ฐ
- feat: ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๊ฒฝ์ฐ
- refactor: ๋ด๋ถ ๋ก์ง์ ๋ณ๊ฒฝํ์ง ์๊ณ ์ฝ๋๋ฅผ ๊ฐ์ ํ ๊ฒฝ์ฐ
- style: ์ฝ๋ ๊ฐ์ ๊ณผ ์๊ด์์ด ์ฌ์ํ๊ฒ ์ฝ๋๋ฅผ ์์ ํ ๊ฒฝ์ฐ
- design: ์ฌ์ฉ์ UI๋ฅผ ์ถ๊ฐ, ์์ ํ ๊ฒฝ์ฐ (๋งํฌ์
, ํผ๋ธ๋ฆฌ์ฑ ์์
)
- add: ํด๋, ํ์ผ ๋ฑ์ ์ถ๊ฐํ ๊ฒฝ์ฐ
- move: ํด๋, ํ์ผ, ์ฝ๋ ๋ฑ์ ์์น๋ฅผ ์ด๋ํ ๊ฒฝ์ฐ
- rename: ํด๋๋ช
, ํ์ผ๋ช
๋ฑ์ ์์ ํ ๊ฒฝ์ฐ
- remove: ํด๋, ํ์ผ, ์ฝ๋ ๋ฑ์ ์ญ์ ํ ๊ฒฝ์ฐ
- assets: ์์
์ ์ถ๊ฐ, ์์ ํ ๊ฒฝ์ฐ
- docs: ๋ฌธ์๋ฅผ ์ถ๊ฐ, ์์ ํ ๊ฒฝ์ฐ
- chore: ์์ ๋ชจ๋ ๊ฒฝ์ฐ์ ํฌํจ๋์ง ์๋ ๊ธฐํ ์์ ์ฌํญ
- ๊ณตํต ์ด์๋ง๋ค๊ณ , ๊ณต์ ํ๊ณ ์ถ์ ๋ฌธ์ ๊ฐ ์๋ ๊ฒฝ์ฐ ์ด์๋ก ์์ฑํ์ฌ ๊ณต์ ํ๊ณ ์์
ํ ์ด๋ ฅ ๋จ๊ธฐ๊ธฐ
- GitHub Project Board
๋ฅผ ์ด์ฉํ ์ ์ฒด ์ง๋ ์ํฉ ๊ณต์
- ๋ค๋ฅธ ํ์ ์ฝ๋์ BUG ์ฐพ๋ ๊ฒฝ์ฐ ์ด์ ์์ฑ ํ Assignees
์ง์
- Common
์ปดํฌ๋ํธ ํด๋๋ฅผ ๋ฐ๋ก ๊ด๋ฆฌํ์ฌ ์ฝ๋ ์ฌ์ฌ์ฉ์จ ๋์ด๊ธฐ
- ํ์ด์ง์ ๋์์ธ์ ์ผ๊ด์ฑ๊ณผ ์ฌ์ด ์ ์ง๋ณด์๋ฅผ ์ํด ThemeProvider
๋ฅผ ์ฌ์ฉ
const colors = {
primary : '#374259' ,
secondary : '#b1b5bb' ,
third : '#F2D8D8' ,
gray : '#dbdbdb' ,
bgGray : '#f2f2f2' ,
txtColor : '#767676' ,
warning : '#FD7A6E' ,
white : '#fff' ,
} ;
const theme = { colors } ;
export default theme ;
- ์๊ท๋ชจ ํ๋ก์ ํธ ๋ฐฉ์ ์ฑํ
์คํ๋์
๋ก๊ทธ์ธ
ํ์๊ฐ์
& ํ๋กํ ์ค์
ํ
๊ณ์ ๊ฒ์
ํ๋ก์ฐ & ํ๋ก์
๊ฒ์๊ธ ์
๋ก๋
๊ฒ์๊ธ ์์
๊ฒ์๊ธ ์ญ์
๋๊ธ ์์ฑ
๋๊ธ ์ญ์
๋๊ธ ์ ๊ณ
์ํ ๋ฑ๋ก
์ํ ์์
์ํ ์ญ์
ํ๋กํ ์์
404
์ง๋
์ค์๊ฐ ์ด๋ฉ์ผ, ๊ณ์ ID ์ค๋ณต ๊ฒ์ฌ ์คํ
useEffect ( ( ) => {
if ( ! [ 'email' , 'accountname' ] . includes ( id ) ) return ;
if (
( id === 'email' && ! EMAIL_REGEX . test ( formData . email ) ) ||
( id === 'accountname' && ! ID_REGEX . test ( formData . accountname ) ) ||
formData [ 'accountname' ] === userAccountname
// ํ๋กํ ์์ ํ์ด์ง์์ ํ์ฌ ๋ก๊ทธ์ธํ ์ ์ ์ accountname์ธ ๊ฒฝ์ฐ ์ด๋ฏธ ๊ฐ์
๋ ๊ณ์ ์ด๋ผ๋ ์๋ฌ ๋ฉ์ธ์ง๋ฅผ ๋ณด์ฌ์ฃผ์ง ์๊ธฐ ์ํจ
) {
return ;
}
const errorMsg = id === 'email' ? '์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ ์ฃผ์ ์
๋๋ค.' : '์ด๋ฏธ ๊ฐ์
๋ ๊ณ์ ID ์
๋๋ค.' ;
const timer = setTimeout ( ( ) => {
checkDuplication ( errorMsg ) ;
} , 300 ) ;
return ( ) => {
clearTimeout ( timer ) ;
} ;
} , [ formData . email , formData . accountname ] ) ;
email
๊ณผ accountname
๋ง ์ค๋ณต ๊ฒ์ฌ๋ฅผ ์งํํ๊ธฐ ๋๋ฌธ์ id
๊ฐ ๋ค๋ฅธ ๊ฐ์ด ๋๋ฉด return
์ ํฉ๋๋ค.
๊ทธ ๋ค์ formData.email
, formData.accountname
์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์คํ์ด ๋ฉ๋๋ค.
์กฐ๊ฑด๋ฌธ์ ํตํด ์
๋ ฅ๋ ์ด๋ฉ์ผ๊ณผ ๊ณ์ ID์ ํ์์ด ์ฌ๋ฐ๋ฅธ์ง ํ์ธํ ๋ค, formData.accountname
์ด ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๊ณ์ ID์ ์ผ์นํ์ง ์๋์ง ํ์ธ ํ, ๋ง์ฝ ์กฐ๊ฑด์ ํด๋นํ์ง ์๋๋ค๋ฉด ํจ์๋ฅผ ์ข
๋ฃํฉ๋๋ค.
๊ทธ ์ด์ธ์, ์ค๋ณต๋ ์ด๋ฉ์ผ ๋๋ ๊ณ์ ID ์๋ฌ ๋ฉ์์ง๋ฅผ ์ค์ ํ๊ณ , 300๋ฐ๋ฆฌ์ด ํ์ checkDuplication
ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
timer
๋ณ์์๋ setTimeout
ํจ์๋ก ์์ฑ๋ ํ์ด๋จธ ID๊ฐ ์ ์ฅ๋๋ฉฐ, clearTimeout
์ ์ฌ์ฉํ์ฌ ํ์ด๋จธ ์ทจ์๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
useEffect
์ ๋ฐํ ํจ์๋ ํด๋น ์ดํํธ๊ฐ ์ ๋ฆฌ(clean-up)๋ ๋ ์คํํ๊ณ , ์ฌ๊ธฐ์ ํ์ด๋จธ๋ฅผ ์ทจ์ํ๊ธฐ ์ํด clearTimeout
์ ํธ์ถํฉ๋๋ค.
๋๋ฐ์ด์ฑ ๊ธฐ๋ฅ์ ์ ์ฉํจ์ผ๋ก์จ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ๋๋ง๋ค ์๋ฒ์์ฒญ์ ํ์ง ์๊ธฐ์ ํต์ ๋น์ฉ์ด ๋ฐ์ํ์ง ์์ต๋๋ค.
์ด๋ฏธ์ง ์
๋ก๋๋ฅผ ์ํ ์ปค์คํ
ํ
import { useState } from 'react' ;
import { uploadImages } from '../api/image' ;
const ALLOWED_EXTENSIONS = [ '.jpg' , '.gif' , '.png' , '.jpeg' , '.bmp' , '.tif' , '.heic' ] ;
const MAX_SIZE = 10 * 1024 * 1024 ;
const useImagesUpload = ( ) => {
const [ images , setImages ] = useState ( [ ] ) ;
const onUpload = async ( files , length ) => {
if ( images . length + length > 3 ) return alert ( '์ด๋ฏธ์ง๋ ์ต๋ 3๊ฐ๊น์ง ์
๋ก๋ ํ ์ ์์ต๋๋ค.' ) ;
const formData = new FormData ( ) ;
for ( let i = 0 ; i < length ; i ++ ) {
const file = files [ i ] ;
const fileExtension = file . name . split ( '.' ) . pop ( ) . toLowerCase ( ) ;
if ( ALLOWED_EXTENSIONS . includes ( `.${ fileExtension } ` ) && file . size <= MAX_SIZE ) {
formData . append ( 'image' , file ) ;
}
}
try {
const data = await uploadImages ( formData ) ;
const filenames = data . map ( ( data ) => data . filename ) ;
setImages ( ( prev ) => [ ...prev , ...filenames ] ) ;
} catch ( error ) {
console . log ( error . message ) ;
}
} ;
const onDelete = ( index ) => {
setImages ( ( prevImages ) => {
const updatedImages = [ ...prevImages ] ;
updatedImages . splice ( index , 1 ) ;
return updatedImages ;
} ) ;
alert ( '์ญ์ ๋์์ต๋๋ค.' ) ;
} ;
return { images, onUpload, onDelete } ;
} ;
export default useImagesUpload ;
์ด๋ฏธ์ง ์
๋ก๋๋ ํน์ฑ์ ํ์๊ฐ์
์ ํ๋กํ ์ค์ , ํ๋กํ ์์ , ๊ฒ์๊ธ ์์ฑ, ์ํ ๋ฑ๋ก ๋ฑ ์ฌ๋ฌ ํ์ด์ง์์ ๋ฐ๋ณต์ ์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค. ๊ทธ๋์ ์ฌ๋ฌ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ ํ ์ ์๋ ์ํ๋ฅผ ์ ์ธํ๊ณ , ์๋กญ๊ฒ ๋ค์ด์ค๋ ์ด๋ฏธ์ง๋ค๊ณผ ๊ธฐ์กด ์ด๋ฏธ์ง๋ฅผ ํฉ์น ๊ฐ์ด 3์ด ๋์ผ๋ฉด ๋ ์ด์ ์
๋ก๋ ํ ์ ์๋๋ก ๊ตฌํํฉ๋๋ค.
FormData
๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ , files
๋ฐฐ์ด์ ์ํํ๋ฉด์ ํ์ฉ๋๋ ํ์ฅ์ ๋ชฉ๋ก๊ณผ ์ด๋ฏธ์ง ์ฌ์ด์ฆ๋ฅผ ๊ฒ์ฌํ ๋ค, ํต๊ณผํ๋ค๋ฉด formData
์ image
๋ผ๋ ํค ๊ฐ์ผ๋ก ํ์ผ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
๊ทธ ๋ค์ uploadImages
ํจ์๋ฅผ ํตํด ์๋ฒ์ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ํ๊ณ , ์๋ฒ ์๋ต์์ ํ์ผ๋ช
์ ์ถ์ถํ์ฌ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๊ณ , setImages
ํจ์๋ฅผ ์ฌ์ฉํด ์ด์ ์ํ๊ฐ์ธ prev
๋ฐฐ์ด๊ณผ ์๋ก์ด ํ์ผ๋ช
๋ฐฐ์ด์ธ filenames
๋ฅผ ํฉ์ณ์ ์ํ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค.
์ด๋ ๊ฒ ํจ์ผ๋ก์จ, ์ด๋ฏธ์ง ์
๋ก๋ ํ์ ์ํ๊ฐ์ ์
๋ฐ์ดํธํ๊ณ React
์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋ ์ ์๋๋ก ํฉ๋๋ค.
๋ฐ๋ณต๋๋ API ์์ฒญ ์์
์ ์ค์ด๊ธฐ ์ํด ํ์ผ ๋ถ๋ฆฌํ๊ธฐ
const BASE_URL = 'https://api.mandarin.weniv.co.kr' ;
export const request = async ( url , options ) => {
try {
const response = await fetch ( `${ BASE_URL } /${ url } ` , {
...options ,
headers : {
'Content-Type' : 'application/json' ,
...options . headers ,
} ,
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
return data ;
}
} catch ( err ) {
console . log ( err ) ;
}
} ;
export const imageRequest = async ( url , options ) => {
try {
const response = await fetch ( `${ BASE_URL } /${ url } ` , { ...options } ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
return data ;
}
} catch ( err ) {
console . log ( err ) ;
}
} ;
๊ฐ์ฅ ๊ธฐ๋ณธ์ด ๋๋ request
ํจ์๋ฅผ ๋ง๋ค์ด ์ค๋๋ค. ๊ทธ ๋ค์, ๋ก๊ทธ์ธ์ auth.js
, ๊ฒ์๊ธ์ post.js
, ์ํ์ product.js
๋ฑ ๊ธฐ๋ฅ๋ณ๋ก ํ์ผ์ ๋ง๋ญ๋๋ค.
import { request } from './request' ;
// ํ์๊ฐ์
export const join = async ( state , formData , image ) => {
return await request ( 'user' , {
method : 'POST' ,
body : JSON . stringify ( { user : { ...state , ...formData , image } } ) ,
} ) ;
} ;
// ๋ก๊ทธ์ธ
export const login = async ( email , password ) => {
return await request ( 'user/login' , {
method : 'POST' ,
body : JSON . stringify ( { user : { email, password } } ) ,
} ) ;
} ;
// ํ ํฐ ๊ฒ์ฆ
// ...
// ์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ(๋๋ ๊ณ์ )์ธ์ง ๊ฒ์ฌ
export const validateForm = async ( id , formData ) => {
return await request ( `user/${ id } valid` , {
method : 'POST' ,
body : JSON . stringify ( { user : { [ id ] : formData [ id ] } } ) ,
} ) ;
} ;
ํ์ผ ๋ด์์ ํ์ํ ์์ฒญ๋ค ์ฆ, auth.js
๋ ํ์๊ฐ์
, ๋ก๊ทธ์ธ ๋ฑ post.js
๋ ๊ฒ์๊ธ ๋ถ๋ฌ์ค๊ธฐ, ์
๋ก๋, ์์ , ์ญ์ , ์ ๊ณ ๋ฑ ๊ฐ๊ฐ์ ํจ์๋ก ๋ง๋ค์ด ์ค๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ฐ๊ฐ์ ํจ์๋ฅผ ์ฌ์ฉํ ๋๋ ๋จผ์ import ํด์ฃผ๊ณ , api๋ฅผ ๊ฐ์ ธ์จ ํ ํ์ํ ์ธ์ ๊ฐ๋ค์ ๋๊ฒจ์ฃผ๋ฉด ๋ฉ๋๋ค.