Giter Site home page Giter Site logo

minwonhaeso / esc_server Goto Github PK

View Code? Open in Web Editor NEW
0.0 0.0 3.0 509 KB

⚽ Easy Sports Club Server ⚽

License: MIT License

Java 99.83% Shell 0.13% Dockerfile 0.04%
aws elasticsearch gitactions-workflow jpa junit5 jwt mockito oauth2 redis spring-security springboot swagger

esc_server's Introduction

⚽ Easy Sports Club ⚽


📎 프로젝트 소개

COVID-19로 위축되었던 스포츠 활동이 다시 활성화 되며, 많은 체육관들이 다시 운영되고 있습니다.

이러한 요즘, 스포츠 활동을 새롭게 시작하기에 앞서, 정보가 부족하여 불편하진 않으셨나요?

위치기반 체육관 검색부터 예약, 대여용품 선택 그리고 선호하는 체육관 찜하기까지 한 번에 이용해 보세요!



🧑‍🤝‍🧑 팀원 소개

Front-End Front-End Back-End Back-End Back-End
권혁민 윤재원 👑차동준👑 박해찬 이소아



📱Demo

🚀 소셜 로그인(카카오, 네이버, 구글) 🔍 주변 체육관 찾기
소셜 로그인 주변 체육관 찾기
💵 체육관 예약 및 결제 📝 리뷰 작성 / 수정
체육관 예약 및 결제 리뷰 작성
🔖 찜하기 🔔 알림 내역
찜하기 알림 내역
🧑🏻‍💻 판매자 회원가입 🚀 판매자 일반 로그인
판매자 회원가입 판매자 일반 로그인
🏋🏿 체육관 등록 / 수정 ✅ 예약 현황
유저_맞춤_술_추천 관리자 체육관 예약 현황

🗂️ Architecture

ESC Structure drawio

💾 ERD 구조

db

🛠️ 기술 스택

Front-End

Back-End






Production & Deploy


actions

Collaboration tool


👍 특장점 기술

✨ 프론트엔드 코드 통일성에 대한 지속적인 고민

  • 코드 컨벤션

    • 협업 및 분업을 원활하게 하기 위해 개발 시 통일성을 부여하고자 많이 고민했어요.
    • TypeScript, Prettier 덕분에 버그를 예방하고 협업 생산성을 높일 수 있었어요.
    • Button Label Input Title과 같은 재 사용성이 요구되는 UI 요소는 Atom 단위로 설계하여 생산성을 높일 수 있었어요
    • Type은 확장이 용이하도록 BaseType을 선언해 중복되는 Property를 줄였어요.
    • 덕분에 200줄의 Type 코드가 60줄로 줄어 들 수 있었어요.
    • 그 외 통일해야 할 부분을 발견하면 즉시 함께 고민하고 실행했어요.
  • 기술

    • RTK 를 사용하여 Client 상태를 관리했어요.
    • RTK Query를 활용하여 Server 상태를 관리하였으며, Caching을 활용하여 통신 비용을 줄일 수 있었어요.
    • 덕분에 응답 다음 작업이나 에러 발생 시에도 통일된 작업을 수행할 수 있었어요.
    • Emotion을 활용한 스타일링 작업 시에 글로벌 스타일 적용과 Typo, Palette로 선언한 변수를 이용하도록 협의하여 통일성을 부여했어요.

✨ ElasticSearch를 활용한 검색 기능

보다 빠른 검색 기능을 제공하기 위해 주변 체육관 검색에 ElasticSearch를 적용하였습니다.

  • RDMS에서 Like 검색 및 Match 보다 빠른 속도로 검색 결과를 제공합니다.
  • 데이터 공간을 절약할 수 있으며, 컬럼을 동적으로 정의하여 필요한 데이터만 넣게 되어, 데이터 공간 및 CPU 사용량을 절약할 수 있습니다.
  • ES는 HTTP를 통해 JSON 형식의 RESTful API로 호출하기 때문에 여러 환경에서 적용이 가능합니다.

✨ JWT와 OAuth2를 활용한 소셜 로그인

  • 구글, 네이버, 카카오에서 제공하는 Authorization Server를 통해 회원 정보를 인증하고 Access Token을 발급 받습니다.
  • 서버 간의 통신이 잦은 경우, Access Token을 자주 주고 받을 수 밖에 없고, 토큰이 유효한지 확인해 주어야 합니다.
  • 해당 과정에서 Auth 서버에 유효성 검증 확인을 위해 요청할 때마다 병목 현상으로 인해 서버의 부하가 발생할 수 있습니다.
  • Claim 기반 방식인 JWT를 통해 Auth 서버에 검증 요청을 보내야했던 과정을 생략하고, 각 서버에서 API 요청이 들어오면 Auth 서버가 아닌 애플리케이션 서버에서 토큰 유효성 검사를 통해 사용자 인증을 거치도록 설정하였습니다.

🚀 트러블 슈팅

🧑🏻‍💻 프론트엔드

🛠 Kakao Map SDK 가독성에 따른 코드 리팩토링

  • Problem & Reason

    • useCallback, useEffect 를 함께 사용하여 코드의 가독성이 떨어지는 문제가 있었습니다. 또한, 다른 컴포넌트에서 Kakao Map 기능을 사용 할 때, 다시 map 정보를 불러주어야 하는 문제가 있었습니다.
    • Kakao Map(Function)
    // 컴포넌트.tsx
    const Map = ({ searchResults, onClickMarker }: MapProps) => {
      const kakaoMap = useKakaoMapScript();
      setMarker({ map: kakaoMap, placeInfo: searchResults, clickHandle: onClickMarker });
    
    return (
        <div>
          <div
            id="myMap"
            style={{
              width: '100vw',
              height: '100vh',
              height: 'calc(100vh - 5rem)',
            }}
          ></div>
        </div>
      )
    }
    
    
    // kakaoScript.ts
    const { kakao } = window;
    
    const useKakaoMapScript = (markerData: any) => {
      const [kakaoMap, setKakaoMap] = useState();
    
      useEffect(() => {
        const container = document.getElementById('myMap');
        const options = {
          center: new kakao.maps.LatLng(37.62197524055062, 127.1583774403176),
          level: 4,
        };
        const map = new kakao.maps.Map(container, options);
    
        markerData.forEach((el: any) => {
          // 마커를 생성합니다
          const markers = new kakao.maps.Marker({
            //마커가 표시 될 지도
            map: map,
            //마커가 표시 될 위치
            position: new kakao.maps.LatLng(el.lat, el.lng),
            //마커에 hover시 나타날 title
            title: el.title,
          });
    
          kakao.maps.event.addListener(markers, 'click', function () {
            console.log(el);
          });
        });
    
        setKakaoMap(map);
      }, [markerData]);
    
      return kakaoMap;
    };
    
    export const mapPanTo = (map: any, location: any) => {
      const moveLatLon = new kakao.maps.LatLng(33.45058, 126.574942);
    
      map.panTo(moveLatLon);
    };
    
    export default useKakaoMapScript;

  • To Solve

    • 기존 함수형으로 작성되던 KaKao Map Script를 Class 문법으로 변경 했습니다.
    • 이로 인하여 재사용성이 더 편리해 졌으며 코드의 목적성 또한 명확해졌고, 코드의 가독성이 올라갔습니다.
    • 모든 로직을 무분별하게 함수형으로 추상화 하는 것을 지양하고, 코드의 목적에 따라 다양한 방법으로 추상화 해야 한다고 느꼈습니다.
    • Kakao Map (Class)
    // 컴포넌트.tsx
    const Map = ({ searchResults, onClickMarker }: MapProps) => {
      useEffect(() => {
        kakaoService.initScript();
      }, []);
    
      useEffect(() => {
        kakaoService.setMarker({ place: searchResults, handleClick: onClickMarker });
      }, [searchResults]);
    
      return (
        <div>
          <div
            id="myMap"
            style={{
              width: '100vw',
              height: 'calc(100vh - 5rem)',
            }}
          ></div>
        </div>
      );
    };
    
    const StadiumSearch = () => {
      const handleEnterFetch = (e) => {
      if (e.key === 'Enter') {
          searchStadium(search);
          kakaoService.setClearMarker();
        }
      }
    }
    
    const EditAddress = () => {
    
      const handleSelectAdress = async (data: Address) => {
        // 주소 string -> 위도 경도 변환
        const geoLocation = await kakaoService.getGeoCode(data.address);
      };
    
    }
    
    
    // kakaoScript.ts
    
    const { kakao } = window;
    
    class KaKaoMap {
      map: any = null;
      markers: any[] = [];
    
      initScript() {
        const container = document.getElementById('myMap');
        const options = {
          center: new kakao.maps.LatLng(ZERO_LOCATION.lat, ZERO_LOCATION.lnt),
          level: 10,
        };
        const map = new kakao.maps.Map(container, options);
    
        const zoomControl = new kakao.maps.ZoomControl();
        map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);
    
        this.map = map;
      }
    
      getGeoCode(address: string) {
        const geocoder = new kakao.maps.services.Geocoder();
    
        return new Promise((resolve, reject) => {
          geocoder.addressSearch(address, function (result: any, status: any) {
            if (status === kakao.maps.services.Status.OK) {
              resolve({ lat: result[0].y, lnt: result[0].x });
            } else {
              reject(status);
            }
          });
        });
      }
    
      goToLocation(location: PanToParam) {
        if (!this.map) return;
        const moveLatLon = new kakao.maps.LatLng(location.lat, location.lnt);
    
        this.map.panTo(moveLatLon);
      }
    
      setMarker({ place, handleClick }: setMarkerParam) {
        if (!place) return;
    
        place.forEach((el: any) => {
          // 마커를 생성합니다
          const marker = new kakao.maps.Marker({
            //마커가 표시 될 지도
            map: this.map,
            //마커가 표시 될 위치
            position: new kakao.maps.LatLng(el.lat, el.lnt),
            //마커에 hover시 나타날 title
            title: el.title,
          });
    
          kakao.maps.event.addListener(marker, 'click', () => {
            handleClick(el);
            this.map.setLevel(8);
            this.goToLocation({ lat: el.lat, lnt: el.lnt });
          });
    
          this.markers.push(marker);
        });
    
        this.goToLocation({ lat: place[0].lat, lnt: place[0].lnt });
      }
    
      setClearMarker() {
        this.markers.forEach(marker => {
          marker.setMap(null);
        });
      }
    
      zoomIn() {
        // 현재 지도의 레벨을 얻어옵니다
        const level = this.map.getLevel();
    
        // 지도를 1레벨 내립니다 (지도가 확대됩니다)
        this.map.setLevel(level - 1);
      }
    
      zoomOut() {
        const level = this.map.getLevel();
    
        // 지도를 1레벨 올립니다 (지도가 축소됩니다)
        this.map.setLevel(level + 1);
      }
    }
    
    const kakaoService = new KaKaoMap();
    
    export default kakaoService;

🛠 Cloudinary Server 이미지 업로드 시점 비용

  • Problem & Reason

    • Image onchange Event 호출 시 Cloudinary 서버에 요청을 보내 응답 데이터를 받아 저장합니다.
      • 이 경우, 사용자가 onChange시 마다 요청을 보내므로, Request Cost가 높아집니다. 또한, Cloudinary 서버는 요청 횟수 1,000번을 넘으면 과금이 부가 되는 문제가 있습니다.
    • submit Event 호출 시 Cloudinary 서버에 요청을 보내 응답 데이터를 받아 온 후 Submit 로직을 실행 합니다.
      • 이 경우, 요청 횟수는 한 번으로 Request Cost는 낮지만, 요청 시점이 동일하며Cloudinary Server 응답을 기다려야 하므로 사용자 경험이 나빠지는 문제가 있습니다.
  • To Solve : 과금에 대한 문제를 줄이기 위해 submit Event로 해결 했습니다.

  • Etc : ‘비용 문제가 없다’ 라고 판단된다면, Image onChange Event 시 Upload를 하여, Request 시점을 나누어 사용자 경험을 증가시킬 수 있다고 생각합니다. 추가로, 변경 이전의 Image에 Delete 요청을 하게 된다면, 효율적으로 Image를 관리할 수 있는 방법이라고 생각합니다.


🧑🏻‍💻 백엔드

🛠 UserDetails Interface serializable

  • Problem

     @Override
     @Cacheable(value = CacheKey.USER, key = "#email")
      public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
          Member member = memberRepository.findByEmail(email).orElseThrow(() -> new AuthException(MemberNotFound));
          return PrincipalDetail.of(member);
      }
    • UserDetails를 implement한 PrincipalDetail class를 serialize(캐시 생성)하는 것은 성공했지만, deserialize(캐시 불러오기)에서 계속 parsing 에러가 발생
      SerializationException: Could not read JSON:cannot deserialize  from Object value
  • Reason

    • Userdetails interface의 Override 메소드가 하나의 변수형태로 json 파일에 저장되기 때문에 Deserialize할 때 해당 변수들을 Override 메소드로 변경할 수 없어 parsing error가 발생하였다.
  • To Solve

    • @JsonIgnore 어노테이션을 통해 Override 메소드들을 제외하고 json 파일로 저장하였다.
    • Before
      {
      "@class": "com.minwonhaeso.esc.security.auth.PrincipalDetails",
      "member": {
          "@class": "com.minwonhaeso.esc.member.model.entity.Member",
          "memberId": 1,
          "email": "[email protected]",
          "name": "해찬",
          "password": "$2a$10$O4967ICeXCld8U2KRGV3GOn7MyS/dbnxloeqssp2.Q2A3GgSm2//2",
          "role": "ROLE_USER",
          "imgUrl": null,
          "nickname": null,
          "type": "USER",
          "status": "ING",
          "providerType": "LOCAL",
          "providerId": "gocks0918"
      },
      "attributes": null,
      "password": "$2a$10$O4967ICeXCld8U2KRGV3GOn7MyS/dbnxloeqssp2.Q2A3GgSm2//2",
      "name": null,
      "enabled": true,
      "authorities": [
          "java.util.Collections$SingletonSet",
          [
              {
                  "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
                  "authority": "ROLE_USER"
              }
          ]
      ],
      "username": "[email protected]",
      "accountNonExpired": true,
      "accountNonLocked": true,
      "credentialsNonExpired": true
      }
    • After
      {
      "@class": "com.minwonhaeso.esc.security.auth.PrincipalDetail",
      "username": "[email protected]",
      "password": "$2a$10$Vw77fNcTVYVp2/OaPJ8ZZOUCyiYWP/hhw25jTUCq2EAnDxL4k.R8e",
      "member": {
          "@class": "com.minwonhaeso.esc.member.model.entity.Member",
          "memberId": 1,
          "email": "[email protected]",
          "name": "해찬",
          "password": "$2a$10$Vw77fNcTVYVp2/OaPJ8ZZOUCyiYWP/hhw25jTUCq2EAnDxL4k.R8e",
          "role": "ROLE_USER",
          "imgUrl": null,
          "nickname": null,
          "type": "USER",
          "status": "ING",
          "providerType": "LOCAL",
          "providerId": "gocks0918"
      },
      "attributes": null
    }

🛠 배포 - HTTP/HTTPS 통신

  • Problem

    • 프론트 서버가 배포된 CloudFront에서 서버에 HTTPS로 요청을 보냈을 때 Connection Refused 현상 발생
  • Reason

    • EC2에는 SSL 인증 처리가 되어있지 않아서 HTTP만 받고 HTTPS를 거부
  • To Solve

    • 로드밸런서를 이용하여 HTTPS(443) 요청을 HTTP(80)으로 리다이렉트 하도록 설정 BUT
    • 이 과정에서 SSL/TLS 인증서가 필요하여 ACM에서 인증서를 발급 BUT
    • 인증서 발급을 위해 도메인이 필요하여 도메인 구입 후 이를 EC2 혹은 EC2와 연결된 로드밸런서에 연결
      • 가비아(도메인 등록 사이트)에서 esc-zero-server.shop 도메인을 구매
      • Route 53에 도메인을 등록하고 로드밸런서 및 EC2(IP주소)와 연결

esc_server's People

Contributors

devycha avatar jaewon0521 avatar phc09188 avatar soa-lee avatar

esc_server's Issues

Fix : 리프레쉬 토큰 발급 요청 오류

Purpose

  • accessToken이 만료되어 오류 응답을 받은 프론트에서 refreshToken으로 재요청 할 때 이미 만료된 토큰을 갖고 요청하게 되면 전체 Filter처리에 걸려 오류가 똑같은 accessToken이 만료되었다는 응답을 받는 오류가 있었음

Progress

  • accessToken 필터 처리 부분에서 /members/auth/refresh-token 엔드포인트를 제외하는 설정 추가

Fix : accessToken 재발급 관련 username 받아오는 로직 오류

Purpose

accessToken을 재발급하는 로직에서 이미 만료된 accessToken으로 현재 접속한 사용자에 접근하여 username을 받아오는 로직에서 오류가 발생하였음

Progress

  • refreshToken을 이용해서 토큰 재발행하는 부분에서 현재 접속한 사용자의 principal 가져오는 것이 오류를 발생하고 있어서 refreshToken안에서 username을 받아오도록 메서드 추가

Feat : 체육관 평점 scheduling

Purpose

  • 리뷰 작성 시, 평점과 코멘트를 함께 입력한다.
  • 체육관 조회 시, 체육관 리뷰 평점 평균을 확인할 수 있는 UI를 구성하고 있다.
  • 스케줄링을 통해 매일 정해진 시각에 평점을 업데이트할 수 있도록 한다.

Progress

  • 체육관 평점 Scheduling 구현

Fix : 리뷰 관련 API 수정

Purpose

리뷰 관련 API 수정 요청 사항입니다!

Progress

  • 리뷰를 시간 순서대로 정렬하려면 createdAt을 LocalDateTime으로 써야할 것 같습니다.
  • 리뷰에서 멤버의 imgUrl도 보내주시면 리뷰에서 사용자 이미지도 같이 띄워줄 수 있을것같습니다!
  • 댓글에서 수정삭제버튼을 보일지 말지 차이를 주기 위해서 리뷰 넘겨주는 response에 현재 접속한 멤버의 id값도 넘겨주면 좋을 것 같습니다.

Fix : 회원가입 관련 이슈

Purpose

Postman으로 중복된 이메일과 정상 등록된 인증키값 입력시
이메일이 중복돼서 에러가 나서 회원가입이 정상적으로 이루어지지 않지만
인증키값은 사라지게 되는 현상이 있습니다!
프론트에서는 중복검사 -> 인증메일 발송 -> 인증키와 함께 회원가입이 지켜지지만
서버쪽에 바로 쏘는 경우(Postman) 발생하는 문제인 것 같습니다

Progress

  • 서버쪽에서도 회원가입시에 이메일 중복을 확인하는 로직이 추가되면 좋을 것 같습니다!

Fix - UserDetails interface serialize Issue

Problem

@Override
@Cacheable(value = CacheKey.USER, key = "#email")
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    System.out.println(email);
    Member member = memberRepository.findByEmail(email).orElseThrow(() -> new AuthException(MemberNotFound));
    return PrincipalDetail.of(member);
}

해당 메소드에서 캐시가 생성(serialize)은 되지만 불러오기(deserialize) 작업을 요청하면 클래스를 생성할 수 없다는 에러가 발생
SerializationException: Could not read JSON:cannot deserialize from Object value

Reason

UserDetails interface에 기본적으로 필요한 username과 password 관련 변수가 존재하지 않았고, 불필요한 내용들에 대해 JsonIgnore처리가 되어있지 않았다.

To solve

  • PrincipalDetail 클래스 구성에 username과 password를 추가
  • 필요하지 않은 정보들은 @JsonIgnore 처리

Feat : 체육관 권한 설정

Purpose

체육관에 사용자(매니저) 연결

Progress

  • 체육관에 사용자(매니저) 정보 추가
  • 매니저 타입의 사용자만 체육관 등록이 가능하도록 수정
  • 체육관, 이미지, 태그, 대여 용품 Update, Delete시에 권한 체크

Fix : 예약 생성시 대여 용품에 대한 로직 수정 필요

Purpose

  • 예약 생성시에 다른 체육관으로 등록된 itemId를 가지고 대여 등록을 하게 되면 오류를 띄워주는 로직이 필요할 것 같습니다!
    제가 이전에 작성한 createReservation에 구현했는데 참고 부탁드리겠습니다!
  • 예약 시간대 형식이 옳바르지 않은 경우 예외 처리해주는 로직이 필요할 것 같습니다!
  • 예약 생성이 완료되면 알림 생성하는 코드도 필요할 것 같습니다!

Progress

  • [ ]

체육관 태그 Entity 관련 수정 예정입니다!

Previous


체육관마다 새로운 태그를 계속 생성하였음

Change


태그 테이블의 name 속성을 unique로 사용하여 변경할 예정

Additional Comment


해찬님 의견: Stadium과 Tag 사이의 연결 테이블을 생성하고 단방향 맵핑으로 진행하는게 좋을 것 같습니다(Tag 태이블도 name을 unique key로 관리할 예정입니다.)

Feat : 리뷰 API 구현

Purpose

체육관 사용 리뷰(후기) + SSE

Progress

  • 체육관 리뷰 세팅
  • 체육관 리뷰 조회
  • 체육관 리뷰 등록
  • 체육관 리뷰 삭제
  • 체육관 리뷰 수정
  • 체육관 리뷰 작성 케이스 고도화
  • Logging 처리
  • Swagger Docs 작성
  • 리뷰 작성 시, 알림 구현

이메일 중복검사 / 이메일 인증 / 회원가입까지 프론트 통합한 뒤 발생한 예외들 정리합니다!

통합시 발견한 소소한 것들... 🫡

어제 혁민님이랑 오류 해결하면서 발생했던 소소한 오류들 정리했습니다! 시간나실때 살짝쿵 고쳐주시면 좋을 것 같아요!

** 참고로 테스트 할 때는 CORS 설정해서 전부 오픈하고 회원가입에서 REDIS 안에 이메일 인증키 비교하는 부분은 제외하였습니다! **

OAuth2를 활용한 소셜 로그인

Purpose

OAuth2를 활용한 사용자 인증 및 인가 구현

Progress

  • Google, Naver, Kakao 소셜 로그인 연동
  • 회원 정보 요청 성공 시, 회원 가입 진행
  • 로그인 성공 시, JWT 발급 및 토큰 전달
  • RefreshToken 및 회원 정보 전달을 위한 API 작성
  • 예외 처리
  • POSTMAN을 이용한 OAuth2 인증 테스트
  • 프론트 - 백 연동 테스트

Test : 리뷰 기능 테스트

Description

  • 체육관 리뷰 테스트 코드 작성
  • 컨트롤러(WebMvc) 및 서비스(Mockito)

Progress

  • 컨트롤러 및 서비스 테스트
  • 리뷰 조회
  • 리뷰 생성
  • 리뷰 수정
  • 리뷰 삭제

Fix - OAuth2 JWT RefreshToken 전달 로직 변경

Description

  • 기존에는 OAuth2를 통해 소셜 로그인에 성공하였을 때, short-lived 쿠키를 통해 RefreshToken을 저장할 수 있도록 하였다.
  • 하지만 HttpOnly 설정으로 프론트에서 쿠키 접근이 불가능한 상황으로 추가적인 요청을 통해 RT를 프론트로 전달할 수 있도록 한다.
  • 토큰뿐만 아니라, 로그인 성공 시 추가적인 회원 정보도 전달해줘야하기 때문에 OAuth2 로그인 성공 시, 추가 로직이 필요하다.
  • AccessToken을 전달하는 방법은 기존의 방법을 유지한다.

Progress

  • 쿠키 관련 Util 삭제
  • RefreshToken 및 회원 정보를 전달한 API 작성
  • 예외 처리
  • postman을 통한 http 통신 테스트
  • 서버 배포 후, OAuth2 URL 설정 변경

Fix : API 스펙 수정 및 유저 관련 에러 핸들링 처리

Purpose

<동준님, 해찬님>

  • stadium 검색, StadiumDetail(체육관 상세), 찜하기 내역 GET API Response 데이터 내부 Like boolean 데이터 추가해서 던져주시면 감사하겠습니다.
  • 이전에 해결했던 로그인 유지 관련 이슈 다시 발생하여 확인부탁드립니다.

<해찬님>

  • 이메일 중복검사 에러 코드 수정 부탁드립니다. 프론트 쪽 catch 문에서 error handling이 되지 않고 있어, 지난 번 비밀번호 찾기 인증코드 확인 단계에서 진행했던 errorCode 변경과 동일하게 해주시면 됩니다.
  • 회원탈퇴 버그 발견되어 로직 수정했는지 확인 부탁드립니다.

<동준님>

  • Manager Reservation User API 생성해주시면 감사하겠습니다. (와이어프레임 Manager Stadium Reservation 참고 부탁드립니다. 유저에 대한 상세 정보는 일반 사용자의 예약 내역 상세보기 데이터와 동일합니다.)

Progress

Feat : 체육관 예약 API 구현

Purpose

체육관 예약 + 예약 알림 기능 구현

Progress

  • 체육관 예약 도메인 셋팅
  • 내 예약 목록 조회
  • 체육관 상세 정보 조회
  • 체육관 예약 페이지(날짜별)
  • 체육관 예약 상세 정보 조회
  • 체육관 예약 취소
  • 체육관 예약 가격 조회(날짜별)
  • 체육관 예약 알림
  • 체육관 예약 완료

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.