Skip to content

๐Ÿ’ผ ๋ช…ํ•จ ์ œ์ž‘ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ (React, Firebase, Cloudinary, PostCSS)

Notifications You must be signed in to change notification settings

lechhw/Business-Card-Maker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

68 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ’ผ Business Card Maker

Intro

React ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž‘์—…ํ•œ ๋ช…ํ•จ ์ œ์ž‘ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค.
Firebase ์˜ Authentication ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ ์†Œ์…œ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€๊ณ ,
์ด ๊ณผ์ •์—์„œ ์–ป์–ด์˜จ user uid ๊ฐ’์œผ๋กœ Realtime database ์— ๋ช…ํ•จ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ CRUD ํ•  ์ˆ˜ ์žˆ๊ฒŒ๋” ๊ตฌํ˜„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  Cloudinary ๋ฅผ ์ด์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ ์–ธ์ œ ์–ด๋””์„œ๋“  ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋˜ํ•œ ์™ธ๋ถ€๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(dom-to-image, fileSaver) ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘์„ฑํ•œ ์นด๋“œ๋ฅผ png ํŒŒ์ผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


์ž‘์—…๊ธฐ๊ฐ„ (2022.06.09 ~ 2022.06.13)


Skills

  • React
    • React Functional Component
  • React Router
  • PostCSS
  • Firebase
    • Authentication
    • Realtime Database
  • Cloudinary
  • Deploy: Netlify

Preview

Login


Maker


Save & Search


Responsive Web


Solution

โœ… Database

syncCard() ํ•จ์ˆ˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ•ด๋‹น ref(๊ฒฝ๋กœ) ์— ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ๋‹ค๋ฉด ๋“ฑ๋ก๋œ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ถ”ํ›„์— ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ œ๊ฑฐํ•ด์ฃผ๋Š” off() ํ•จ์ˆ˜๋ฅผ ๋ฆฌํ„ดํ•ด์ค๋‹ˆ๋‹ค. ์ด๋Š” maker ์ปดํฌ๋„ŒํŠธ์˜ useEffect() ์•ˆ์—์„œ ์ฝœ๋ฐฑํ•จ์ˆ˜ํ˜•ํƒœ๋กœ ๋ฆฌํ„ด์„ ํ•ด์ฃผ์–ด ์–ธ๋งˆ์šดํŠธ๊ฐ€ ๋˜๋ฉด off() ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

// card_database.js
import { firebaseDatabase } from './firebase';

class CardDatabase {
  //..์ƒ๋žต

  syncCard(userId, onUpdate) {
    const ref = firebaseDatabase.ref(`${userId}/cards`);

    ref.on('value', (snapshot) => {
      const value = snapshot.val();
      value && onUpdate(value);
    });
    return () => ref.off();
  }
}

export default CardDatabase;

// maker.js
useEffect(() => {
  if (!userId) {
    return;
  }
  const stopSync = cardDatabase.syncCard(userId, (value) => {
    setCards(value);
  });

  return () => stopSync();
}, [userId, cardDatabase]);

โœ… Image uploader(Cloudinary)

file ์„ ์ธ์ž๋กœ ๋ฐ›์•„์™€ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ํ›„ ํ•ด๋‹น ์ด๋ฏธ์ง€ url ์„ ๋ฆฌํ„ดํ•ด์ค๋‹ˆ๋‹ค.

class UploadImage {
  async upload(file) {
    const data = new FormData();
    data.append('file', file);
    data.append('upload_preset', 'docs_upload_example_us_preset');

    const result = await fetch(
      'https://api.cloudinary.com/v1_1/demo/image/upload',
      {
        method: 'POST',
        body: data,
      }
    );

    return result.json();
  }
}

export default UploadImage;

โœ… Save image(dom-to-image, fileSaver)

toBlob() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๊ฐ€ ์ผ์–ด๋‚  ํƒ€๊ฒŸ์„(card) ์„ ํƒํ•˜๊ณ  saveAs() ํ•จ์ˆ˜๋กœ ํ•ด๋‹น ํƒ€๊ฒŸ์„ png ํŒŒ์ผ๋กœ ์ €์žฅ์‹œ์ผœ ์ค๋‹ˆ๋‹ค.

import React, { memo, useRef } from 'react';
import styles from './card.module.css';
import domToImage from 'dom-to-image';
import { saveAs } from 'file-saver';

const Card = memo(({ card }) => {
  const cardRef = useRef();

  const onSaveFile = () => {
    const card = cardRef.current;
    // dom-to-image
    const filter = (card) => {
      return card.tagName !== 'BUTTON';
    };
    domToImage.toBlob(card, { filter: filter }).then((blob) => {
      // fileSaver
      saveAs(blob, `๐Ÿ’ผ ${name} card.png`);
    });
  };

  return (
    <li className={styles.list}>
      <div ref={cardRef}>
        <article className={`${styles.card} ${getTheme(theme)}`}>
          <button className={styles.save} onClick={onSaveFile} title="save">
            <img src="./images/save.png" alt="save" />
          </button>
          //... ์ƒ๋žต
        </article>
      </div>
    </li>
  );
});

export default Card;

โœ… Search

search componenet ์˜ input value๋ฅผ setSearch() ์— ์ €์žฅํ•˜๊ณ , search ์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด card ์˜ name ๊ณผ ๋น„๊ตํ•˜์—ฌ ํ•ด๋‹นํ•˜๋Š” card ๋ฅผ ๋ฆฌํ„ดํ•ด์ฃผ๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด card ์ „์ฒด๋ฅผ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.

import React, { useState } from 'react';
import Card from '../card/card';
import Search from '../search/search';
import styles from './preview.module.css';

const Preview = ({ cards }) => {
  const [search, setSearch] = useState('');
  
  const onChange = (e) => {
    setSearch(e.target.value);
  };

  return (
    <section className={styles.preview}>
      <div className={styles.search}>
        <Search onChange={onChange} />
      </div>

      <ul>
        {!search &&
          Object.keys(cards).map((key) => <Card key={key} card={cards[key]} />)}

        {search &&
          Object.keys(cards)
            .filter((key) => cards[key].name.toLowerCase().includes(search))
            .map((key) => <Card key={key} card={cards[key]} />)}
      </ul>
    </section>
  );
};

export default Preview;

About

๐Ÿ’ผ ๋ช…ํ•จ ์ œ์ž‘ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ (React, Firebase, Cloudinary, PostCSS)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published