GithubHelp home page GithubHelp logo

sol-mi / fastcatch-frontend Goto Github PK

View Code? Open in Web Editor NEW

This project forked from fc-fastcatch/fastcatch-frontend

0.0 0.0 0.0 3.16 MB

숙박 예약 서비스 - 빨(8)리잡아(FastCatch) FrontEnd Repository

Home Page: https://www.fastcatchapp.com/

JavaScript 3.75% TypeScript 67.00% CSS 0.44% HTML 0.43% SCSS 28.38%

fastcatch-frontend's Introduction

✨ 이거 다 되면 팔조 ?! ✨

야놀자 미니 프로젝트 - 숙박 예약 서비스

빨리잡아!


📝 프로젝트 소개

관련 링크

wiki 배포 레포

🐸 테스트 아이디 / 비밀번호

테스트 아이디 1: [email protected] 비밀번호: aaaa1111

테스트 아이디 2: [email protected] 비밀번호: bbbb1111

🎯 개발 기간

2023.11.06 - 11.16

리팩터링 기간: 2023.11.20 - 12.15

🛠️ 기술 스택 및 선택 이유

Language

사용이유
TypeScript를 사용하면 기존 자바스크립트 개발 환경처럼 추상적인 런타임 에러를 핸들링하기보단 정확히 어떤 부분의 타입이 문제가 있는지 파악하기 쉽다는 장점이 있습니다. 또한 실제 배포 시 오류 발생가능성도 낮아 안전한 개발을 할 수 있기 때문에 선택하였습니다.

Development

React

사용이유
React는 부드러운 사용자 경험을 제공하는 대표적인 SPA앱이며, 풍부한 라이브러리와 레퍼런스를 가지고 있기 떄문에 선택하였습니다. 그리고 컴포넌트 단위로 코드를 관리하는 측면에서 협업에도 좋고 재사용성도 높다는 장점을 가지고 있어 선택하였습니다.

Recoil

사용이유
Recoil은 Props drilling을 해소하거나 페이지 간 데이터 공유를 쉽게 할 수 있는 장점이 있습니다. 여러 전역 상태관리 툴 중에 상대적으로 간편한 문법을 가지고 있습니다. 개발 속도도 빠르고 가독성이 좋아 상태들을 파일 별로 나눠 놓았을 때 인지하기 쉽다는 장점으로 선택했습니다.

SCSS

사용이유
변수 선언, import, mixin 등이 가능해 코드의 재활용성을 높일 수 있을 뿐 아니라, Nesting 기능을 통해 코드의 가독성을 높일 수 있기에 선택했습니다.

Axios React query

사용이유
React Query는 캐싱 기능으로 React 애플리케이션에서 데이터를 관리하고 가져오는 데 편리하고, 성능을 향상시키는 데 도움이 될 것을 기대하고 사용했습니다. Axios는 그 자체로도 간편한 비동기 서버 요청 툴이지만, instance를 통해 서버 요청을 가로채서 토큰을 넣어주어 토큰 넣어줄수도 있는 편리한 기능을 갖고 있습니다.

따라서 React Query와 Axios를 결합해 사용하면 더욱 효율적으로 데이터를 관리할 수 있을 것이라 생각했습니다.

Build & CI/CD

사용이유
Vite는 ESM에 최적화되어 있고, 즉각적인 테스트 환경을 제공하므로 개발 중 불필요한 지연이 최소화되어 개발 환경이 더 원활해질 수 있을 것이라 기대했습니다.

Vercel

사용이유
자동 배포를 설정해두어 최종 제출 시에 배포로 인한 문제를 사전 차단하고자 했고, pr을 올릴 때마다 preview할 수 있도록 설정해 디테일한 확인을 가능하게 하고자 했습니다.

Design

Figma

사용이유
최근 디자이너들이 가장 사용하는 UXUI 기획툴입니다. 동시에 시안을 보며 같이 작업하거나 코멘트를 남기기 쉬워서 생산성이 높아 선택했습니다.

Communication

사용이유
버전 관리와 문서화가 용이하기에 선택했습니다. 이슈 관리와 코드 리뷰를 철저히 해 충돌을 최소화한 채로 협업할 수 있었습니다.

사용이유
정해진 시간동안 채널에 상주하여 이슈가 생겼을 때 빠른 공유와 해결이 가능하도록 환경을 구성했습니다. 또한 github과 webkit으로 연동해 pr, issue, 코멘트 생성 시에 빠르게 확인할 수 있도록 해 모든 상황을 서로 공유할 수 있도록 했습니다. 또한 문서화와 데일리 회의록을 기록하기 위한 용도로 노션을 사용했습니다.

✔️ RFP 구현 사항

  • 로그인, 회원가입

  • 전체 숙박 상품 목록 조회

  • 카테고리를 임의 생성하여 분류하여 출력

  • 개별 숙박 상품 상세 소개

  • 숙박 상품 옵션 선택

  • 장바구니 담기

  • 바로 결제하기

  • 장바구니 보기

  • 장바구니에서 주문하기 버튼 클릭 시, 예약(주문) 페이지로 이동

  • 만 14세 이상 이용 동의 (상세 설명서 X, 체크박스로만 간단히 처리)

  • 결제하기 버튼 클릭 시, 상품을 주문한 것으로 처리 (별도 결제 로직 없음)

  • 결제 성공 시 주문 결과 출력

  • 별도 주문 내역 페이지를 통해 주문 내역 확인

🎨 페이지 별 디자인 및 기능

1) 로그인 페이지

refac_login (1) (1)

유효성 검사

  • useForm을 사용해 각 항목에 대한 유효성 체크

유저정보 전역 상태관리

  • 로그인 시 유저정보 전역 상태로 담아 각 페이지에서 사용자 Id로 내정보 조회

에러 메시지 핸들링

  • 실패 응답 값에 따라 존재하지 않는 회원이거나, 비밀번호 불일치 시 toast 알림 노출

2) 회원가입 페이지

refac_signup (1) (1)

유효성 검사

  • useForm을 사용한 각 항목에 대한 유효성 체크

닉네임 중복조회

  • 값의 유무, 숫자 입력 제한, 글자 수에 따라 1차 필터
  • 중복 데이터 서버 요청 후 boolean 응답 값에 따라 사용 가능 여부 알림

조건 미 충족 시 버튼 비활성화

  • 약관 동의 미체크 혹은 닉네임 중복 여부에 따른 버튼 활성화

cypress 테스팅

  • 회원가입 유효성 자동검사

3) 헤더

검색/필터 조회

  • 지역/날짜/인원 조회

    filter

  • 숙박 카테고리 조회 (ex. 호텔, 모텔, 펜션, 게스트하우스)

    category

  • 검색 필터 조회

    search

세부 필터 조회

  • 등록순, 가격 낮은 순, 가격 높은 순, 가나다순 정렬

    detail

  • 숙소 9가지 옵션 다중선택 필터

  • 필터된 데이터 전역 상태관리

로그아웃 모달

  • 모달 포트를 활용한 모달 구현
  • 텍스트, 버튼동작 prop 전달
  • 로그아웃 시 토큰 정보 제거 및 메인페이지 이동

4) 메인 페이지

전체 상품 조회

  • 이름, 이미지, 주소, 숙박 카테고리 표시

  • 예약마감 표시

  • 무한 스크롤 구현 (스크롤에 따라 2개씩 아이템 불러오기)

    infiniteScroll

5) 숙소 상세 조회 페이지

개별 숙박 상품 상세 조회

  • 이름, 주소, 예약 가능 여부, 전화번호, 소개, 위치, 옵션, 객실 표시

  • useQuery를 이용해 효율적으로 데이터 관리 전체디자인

  • kakao map api 연동 및 편의시설(은행,카페,편의점) 조회 기능 지도

룸 조회

  • swiper library & lodash debounce

  • 일정과 인원수에 따른 룸 조회 및 예약 마감 표시 인원수 변화

  • 일정 및 인원수에 따른 가격 변동 가격변화

장바구니 담기

  • debouncing
  • 비로그인 시 로그인 페이지로 이동
  • 성공/실패를 판단할 수 있는 toast 장바구니담기

즉시 결제하기

  • 전역으로 상태 관리
  • 비로그인 시 로그인 페이지로 이동 즉시결제

6) 장바구니

숙소 별 장바구니에 담은 객실 분류

장바구니 객실 총 갯수와 총 금액 계산 로직 구현

recoil useSetRecoilState를 사용하여 결제 페이지로 상태 전달

장바구니 결제

7) 결제 페이지

유효성 검사

  • useValidation 커스텀 훅을 만들어서 항목에 대한 유효성 체크 유효성 검사

utils 제작

  • 날짜, 가격 같은 계산이 필요한 부분들을 유틸 함수로 제작

쿼리 스트링으로 데이터 구분

  • 쿼리 스트링을 통해 데이터를 구분하여 어디서 온 데이터인지 체크하여 그에 맞는 API 요청

구매 후 처리

  • 구매 후 성공과 실패 처리를 구분하였고 실패 시 사유를 사용자에게 제공함으로써 사용자 경험 개선 결제 과정 결제 처리

8) 마이 페이지

수정 기능

  • 내 데이터를 불러와서 수정과 유효성 체크를 한 후 API 요청

axios instance

  • axios instance를 만들어서 응답과 요청 전에 interceptor를 통해 토큰이 필요한 상황을 분리하지 않도록 하여 개발 비용을 줄이고 에러 핸들링을 쉽게 하도록 제작

9) 주문 목록 페이지

더보기, 삭제 기능

  • 서버와의 통신을 통해 데이터를 주고 받고 react-query를 사용하여 데이터가 삭제되면 바로 서버와 통신하여 업데이트하도록 제작 주문 목록

🖥️ 아키텍처

아키텍처

📂 폴더 구조

📂 src
┣ 📂 api
┣ 📂 assets                   # 폰트, 이미지 ,아이콘
┣ 📂 components               # 공용 컴포넌트
┃  ┣ 📂 common
┃  ┃  ┣ 📂 badge
┃  ┃  ┣ 📂 modal
┃  ┃  ┣ ...
┃  ┣ 📂 roomName
┃  ┣ 📂 termsAgreement
┃  ┣ ...
┣ 📂 constant
┃  ┣ validation
┃  ┣ ...
┣ 📂 hooks                    # 커스텀훅
┣ 📂 mocks
┣ 📂 pages                    # 페이지 컴포넌트
┃  ┣ 📂 accommodation
┃  ┣ 📂 basket
┃  ┣ 📂 home
┃  ┣ 📂 members
┃  ┣ 📂 order
┃  ┃  ┣ 📂 bookInformation
┃  ┃  ┣ 📂 orderItem
┃  ┣ ...
┣ 📂 routes
┣ 📂 states                   # 전역상태
┣ 📂 styles                   # 스타일테마
┣ 📂 types                    # 타입스크립트 공용 인터페이스
┣ 📂 utils
┣ App.tsx
┣ main.tsx

🧑🏻‍💻 팀 소개 및 역할

팀장 | 어준혁 팀원 | 고솔미 팀원 | 윤태관 팀원 | 이예인 팀원 | 정범환
@Eojoonhyuk @SOL-MI @tkyoun0421 @furaha707 @Bumang-Cyber
  • 개발 환경 설정 (vite-react)
  • 공통 컴포넌트 제작(Badge)
  • 메인 페이지 숙소 레이아웃 제작
  • 장바구니 페이지
  • 라우터 설정
  • MSW 세팅
  • 숙소 상세 조회 페이지
  • 공통 컴포넌트(Button,Input) 개발
  • utils 개발
  • 카카오맵 연동 및 검색 기능
  • 구매 페이지
  • 마이 페이지
  • 구매 목록 페이지
  • axios instance
  • custom hook
  • util function
  • 로그인 페이지
  • 회원가입 페이지
  • 로그아웃 모달
  • 메인 페이지 세부 필터 조회
  • UX/UI디자인
  • 메인페이지/헤더 검색 필터 조회 기능
  • Recoil 환경 설정
  • cypress 테스팅

에러 해결 방법

  • 주요 에러 내용
    1. CORS에러

      1. 동일 출처 정책(Same Origin Policy)을 지키지 않을 시 발생하는 에러입니다. 브라우저는 보안 상의 이유로 교차 출처 요청을 제한합니다.
    2. Mixed Contents 에러

      • 최초 html이 안전한 https 연결을 통해 되는 반면 다른 리소스(이미지, 동영상, 스타일 시트, …)들은 안전하지 않은 http 연결을 통해 로드를 시도할 때 발생하는 에러입니다.

  • 에러 해결 방법
    1. CORS에러 해결 방법

      • 프론트엔드는 localhost와 5173이란 포트로 요청을 보내고 서버는 EC2 서버 호스트와 80 포트에서 응답하여 발생하였습니다. 백엔드 단에서 프론트 서버의 출처를 white list에 추가하여 해결해야 되는 문제였습니다. 그래서 백엔드에서 localhost:5173 으로 오는 요청을 열어 두어 문제를 해결하였습니다.
    2. Mixed Contents 에러 해결 방법

      • api를 http 혹은 https로 통일했어야 하는데 이를 혼합하여 api를 설계했기 때문에 발생한 문제였습니다. 이를 해결하기 위해서는 api를 완전 다시 설계하거나 혹은 프록시 서버를 이용한 우회로로 해결하는 방법을 시도해야 됐습니다. 그래서 프록시 서버를 돌리기 위해 Let's Encrypt라는 무료의 TLS/SSL 인증서를 발급받으려 했으나 이에 어려움을 겪고 결국 서버에서 도메인을 사서 로드밸런서를 연동하여 https 프로토콜 통신을 가능하게 했습니다.

✍ 회고

고솔미
  • 짧은 프로젝트 기간으로 인해 백엔드와 더욱 긴밀한 소통을 해볼 수 있는 값진 경험이 되었습니다. 원활한 소통을 위한 방법과 개발 기간을 효율적으로 사용하는 방법에 대해 많은 고민을 해보았습니다. 이슈나 궁금증이 생겼을 때 즉각적으로 소통할 수 있는 창구를 만들어 놓는 것이 중요한 것 같습니다. 저희는 디스코드에 항상 상주해 실시간으로 소통할 수 있도록 했고, 이는 프론트엔드 사이에서도, 프론트&백 사이에서 이슈가 생겼을 때 모두 빠르게 참여해 해결할 수 있는 환경을 만들어주었습니다.또한 코드리뷰를 매번 꼼꼼히 하는 환경을 조성해서 서로의 코드를 파악하고 있다보니 전체적으로 코드의 통일성도 높아지고, 큰 이슈 없이 프로젝트를 마칠 수 있었습니다.

  • lighthouse 점수를 높이는 것을 목표로 리팩토링을 진행해보았는데, 같은 동작을 하더라도 어떤 방식으로 코드를 짜고, 설계하는지에 따라 점수 차이가 큰 것이 와닿았었습니다. 코드 스플리팅과 font 최적화가 가장 큰 영향을 주었습니다. font가 주는 부담을 줄이기 위해서는 dynamic subset과 preload를 적용해보았습니다.


- 같은 동작을 하는 코드를 다양한 방식으로 다시 짜보고, 백엔드와 일정 조율에 있어서 이슈가 생겼을 땐 MSW도 도입해보며 성장하는 시간을 보낸 것 같습니다. 또한 재활용할 수 있는 공통 컴포넌트들을 여럿 제작해보며 확장성이 좋은 컴포넌트를 만드는 힘을 기를 수 있었습니다. 팀원분들께서 좋은 코드를 많이 짜주셔서 코드리뷰를 하면서도 많이 배울 수 있었습니다. 또한 늦은 시간에도 정성스런 답변을 해주신 멘토님께도 감사드립니다 :)
어준혁
- 훌륭한 프론트엔드와 백엔드 팀원들과 함께해서 정말 만족스러운 프로젝트 경험이었습니다. 매일 아침 데일리 스크럼을 통해 현재 진행 상황을 서로에게 공유하고 오늘의 목표를 세우는 것은 팀의 협업과 작업 효율성을 높이는 데 큰 도움이 되었습니다.

- 특히, 백엔드 팀원들과의 소통은 매우 유익한 경험이었습니다. 건의 사항이나 궁금한 점을 정리하고 토론하는 시간을 가져서 프로젝트에 대한 이해도를 높일 수 있었습니다. 또한, 빠른 디자인 결과물과 공통 컴포넌트의 활용은 레이아웃 작업을 빠르게 완료할 수 있게 해주었습니다.

- 팀원들 간의 협업은 에러 발생 시 빠르게 공유하고 해결하는 데 큰 역할을 했습니다. 코드리뷰를 통해 서로의 코드를 검토하고 더 나은 방향으로 나아갈 수 있도록 도움을 주고 받았습니다.

- 팀장이 되어서 많은 걱정과 고민이 있었지만, 팀원들의 열정과 노력 덕분에 모든 것이 순조롭게 진행되었습니다. 특히, 바쁜 일정 속에서도 항상 친절하게 멘토링 질문에 답변해주신 멘토님에게도 큰 감사의 말씀을 전하고 싶습니다.

- 이 경험을 통해 팀원들과의 협업, 소통, 그리고 문제 해결 능력이 크게 향상되었습니다. 다양한 경험을 나누어주셔서 정말 감사합니다. 앞으로도 함께 성장하며 더 좋은 결과물을 만들어 나갈 수 있기를 기대합니다.
이예인
- 효율적인 작업을 위한 초기세팅과 코드컨벤션이 잘 지켜진 프로젝트였습니다. 작업 시작할 때, 재사용성이 높은 컴포넌트들을 나눠서 구현하기 시작했고 작업하면서도 util 함수를 작성해서 공유함으로써 작은 단위로 작업한다는 느낌을 받았습니다.

- 그래서 스스로 깔끔한 코드를 작성하고 싶은 마음이 들었던 것 같습니다. 좋았던 문화중에 코드리뷰 문화를 통해 내 파트만 알고있는 것이 아닌 전체적인 코드에 대한 파악이 가능했던 점이 좋았고 또 유용한 정보들도 팀원들의 코드를 통해 많이 얻게 되었습니다.

- 기본에 충실하고 단단한 서비스를 만들자는 팀원들과의 다짐이 이뤄진 것 같아 매우 뿌듯하고 백엔드 수강생과 협업 사이클을 한 번 돌고 나니 프론트의 역할에 대해 이해도가 많이 올라갈 수 있었던 시간이었습니다. 백엔드 수강생분들, 팀원분들, 그리고 밤늦게까지 도와주신 멘토님 모두 고생하셨습니다!
윤태관
- 이전 프로젝트를 해오면서 초반 기획과 설계가 중요하다는 것을 깨달음을 느꼈고, 그 덕에 프로젝트 진행하는데 초반에 시간 투자를 한 만큼 진행하는데 수월한 경험을 하였습니다. 매일 하는 데일리 스크럼 덕에 프로젝트의 방향성을 잡을 수 있었고 pr을 할 때마다 코드 리뷰를 하는 진행 방식 덕분에 모르는 부분과 아는 부분을 조원분들과 공유하고 나눌 수 있는 경험을 하게 되어서 저에게 큰 도움이 되었습니다.

- 처음으로 백엔드와 함께 하는 프로젝트여서 초반에는 어떤 식으로 진행해야되고 어떤 식으로 소통하고 프로젝트를 진행해야될지 고민이였지만 너무나 열심히 해주시는 백엔드 팀원분들과 프론트엔드 팀원들이 있어서 잘 진행되었던 것 같습니다.

- 작업 막바지쯤에 갑자기 터진 CORS 오류로 인해서 이를 해결하기 위해 많은 공부를 하게 되었고, 네트워크 지식에 대한 필요성을 느끼게 된 경험이였습니다. 이 과정에서 늦은 시간까지 정말 많은 도움을 주신 유형철 멘토님께 정말 감사드립니다.. 이번 프로젝트를 통해 다시 한번 제 부족함을 느끼게 되었지만, 좋은 분들과 좋은 프로젝트 경험을 통해 한 단계 더 성장하였습니다. 감사합니다!
정범환
  • 여행 숙박 예약이라는 큰 주제를 2주 안에 구현한다는 매우 밀도 높은 과제였습니다. 이를 위해 수많은 논의가 필요했고 제 역량보다 더 도전적인 과제들을 구현하는 일의 연속이었습니다. 그 과정에서 좋은 팀원분들이 매우 소통을 원활히 이끌어주셔서 해낼 수 있던 것 같습니다. 밤늦게까지 도와주신 멘토님께도 감사의 말씀 드립니다!

  • 전역 상태 관리에 크게 경험치를 쌓을 수 있는 프로젝트였습니다. 여행 지역, 일정, 인원등을 여러 페이지에서 접근 가능한 전역데이터로 관리했는데 유저가 시험삼아 바꿔본 값이 페이지 이동 시 유지되는 페인 포인트가 있었습니다. 그래서 사용자가 '실제로 적용 버튼을 누르기 전까지의 값'과 '가장 최근 적용된 값'을 두 개 다 가지고 있다가 적용버튼을 누르지 않으면 가장 최근 적용된 값으로 보이게 설계했었는데, 이 부분에서 되게 많은 고민과 고충이 있어서 기억에 남네요. 좋은 경험치를 쌓을 수 있는 프로젝트였습니다!

📍 컨벤션

커밋 컨벤션
feat 새로운 기능 추가
fix 버그 수정
env 개발 환경 관련 설정
style 코드 스타일 수정 (세미 콜론, 인덴트 등의 스타일적인 부분만)
design css 등 디자인 추가 및 수정
refactor 코드 리팩토링
comment 주석 추가/수정
docs 내부 문서 추가/수정
test 테스트 추가/수정
chore 빌드 관련 코드 수정
rename 파일 및 폴더명 수정
remove 파일 삭제


리팩터링 추가 / 개선

이름 내용
어준혁       1) API 요청 react-query로 변경
2) 공통 컴포넌트 폴더 구조 수정
고솔미 1) 폰트 로드 방식 수정으로 성능 최적화 (lighthouse 점수 개선 55점→96점)
2) FOUT 방식 적용
3) type 및 interface 정리, 에러/로딩 페이지 적용 등 리팩토링을 통한 가독성 및 완성도 향상
4) 반응형 스타일링
이예인 1) 토큰 갱신 로직 구현
2) 로그아웃 API 호출 및 토큰 해제
3) 리프레시 토큰 만료 시 재인증 유도
4) 반응형 스타일링 업데이트
윤태관 1) React.memo와 useCallback을 이용하여 필요하지 않은 재랜더링을 막아 성능 최적화
2) 필수 체크 박스가 체크되지 않았을 때 opacity로 구분 짓는것이 아니라 아예 체크가 안된 박스를 사용하여 사용자 경험성 향상
3) react-query를 사용하여 캐시된 데이터가 변경 되었을 때 refetch 함수를 사용하여 서버 통신을 통해 바로 화면에 반영 (주문 목록페이지 → 예약 아이템 취소 → 취소 목록에 아이템 생성)
정범환 1) 헤더 반응형을 고려하여 태블릿 사이즈까지 지원, 브레이크 포인트 이하일 시 주요 버튼 텍스트 간소화.
2) 디테일 필터의 버그 수정(현재 선택되어진 카테고리 기준으로 필터링이 되는게 아니라 전체 기준으로 필터링 되는 버그 수정)
3) lottie.js를 활용한 에러, 로딩 중 인터랙션 추가
4) intersection observer, 리액트 쿼리 리패치보다 recoil 업데이트를 먼저 이루어지게 하기위한) 순서보장을 위해 썼던 setTimeout을 모두 제거

fastcatch-frontend's People

Contributors

sol-mi avatar bumang-cyber avatar furaha707 avatar tkyoun0421 avatar eojoonhyuk avatar dt-career avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.