그룹원들과 홈트 시간을 공유하며 쉽고 꾸준한 운동 습관을 길러주는 홈트 플랫폼
[FE] https://hometry.vercel.app/
[BE] https://home-try.13.125.102.156.sslip.io
홈트라이는 혼자 운동하기 어려운 이들을 위해, 함께 운동하는 느낌과 동기를 제공하는 홈트 플랫폼입니다. 코로나 이후 홈트 붐이 일어났지만 집에서 혼자 운동하다 보니 동기부여가 어렵고, 쉽게 포기할 수 있습니다. 홈트라이는 이러한 문제점을 해결하고자 시작되었습니다.
- 꾸준한 운동 습관 형성
- 운동 시간을 시각화하고 기록하여 매일 운동을 실천하도록 돕고, 그룹 내 랭킹 시스템과 채팅 기능을 통해 그룹원들과 소통하며 운동의 재미를 더합니다. 👏
- 건강한 커뮤니티 형성
- 그룹을 통해 운동 지식을 공유하고, 서로의 운동 성과를 인증하며 응원함으로써 건강한 커뮤니티 문화를 만들어갑니다. 💬
- 쉬운 접근성
- 모바일로 간편하게 운동 기록과 통계를 확인하고 실시간으로 운동에 참여할 수 있어 언제 어디서든 운동 습관을 기를 수 있습니다. 🌟
그룹원들과 홈트 시간을 공유하며 쉽고 꾸준한 운동 습관을 길러주는 홈트 플랫폼, 홈트라이
- 실시간으로 운동 시간을 측정하여 시각화를 통해, 혼자 집에서도 꾸준히 홈트를 진행하도록 동기 부여 🔥
- 매일 반복되는 운동 사이클 속 재미와 운동 습관 형성 ✨
- 그룹 내 랭킹 및 채팅 기능을 통해 그룹원들과 커뮤니케이션이 가능하며, 새로운 운동 지식과 오운완 인증까지! 💪🏻
FE
정서윤(FE 테크리더) | 이도현 |
---|---|
로그인, 메인, 랭킹, 마켓, 마이페이지 | 로그인, 나의 그룹, 그룹 탐색, 채팅페이지 |
BE
정우재(팀장) | 김수랑(BE 테크리더) | 박준형 | 조현서 |
---|---|---|---|
나의 그룹, 그룹탐색 | 회원, 인증, 채팅 | 일기, 태그 | 운동, 마켓 |
2024년 9월 ~ 11월
서버 사이드
데이터베이스
실시간 채팅
인증 및 보안
서버 사이드 렌더링
빌드 도구 및 배포
- 회원, 인증
- 운동
- 일기
- 그룹
- 채팅
- 마켓
- 관리자
-
회원가입 / 로그인
- 사용자가 카카오 계정으로 로그인 할 수 있는 기능입니다.
- 회원가입 시 사용자의 역할은
USER
로 저장됩니다. - JPA의
@PrePersist
를 이용하여 처음 회원 정보를 DB에 insert 할 때 운동 출석일수를 default로 0으로 지정합니다. - 회원가입 시 사용자의 닉네임은 무작위로 지정되며, 추후에 마이페이지에서 닉네임을 변경할 수 있습니다.
- 카카오 Biz 앱 애플리케이션을 사용하여 회원의 이메일을 저장합니다.
- 카카오 로그인은 해당 시퀀스 다이어그램을 따릅니다.
-
마이페이지
- 마이페이지를 조회합니다. 사용자는 자신의 닉네임, 이메일, 출석일 수, 운동 월별/주간 통계를 볼 수 있습니다.
- 닉네임 변경
- 사용자의 닉네임을 최대 32자로 변경할 수 있습니다.
- 사용자는 마이페이지에서 회원 탈퇴 요청을 보낼 수 있습니다.
-
회원 탈퇴
-
사용자가 로그인한 회원을 탈퇴하는 기능입니다.
-
회원 탈퇴는 해당 시퀀스 다이어그램을 따릅니다.
-
- 운동 생성
- 사용자가 새로운 운동을 생성할 수 있는 기능입니다. 사용자가 운동 이름을 입력하여 생성 요청을 보내면 운동이 생성됩니다.
- 운동 생성 시 이벤트 기반 아키텍처를 사용하여 관련된 부가 작업(ExerciseTime 생성 및 초기화)을 별도로 처리합니다. 이를 통해 운동 생성의 주요 비즈니스 로직과 부가 작업을 분리하여 코드의 가독성과 유지보수성을 높였습니다.
- 운동 삭제
- 특정 운동 기록을 삭제하는 기능입니다. 사용자가 잘못 기록한 운동이나 불필요한 운동을 삭제할 수 있습니다.
- 운동 삭제는 Soft Delete를 이용하여 처리되기 때문에, 데이터베이스에서 삭제되지 않고 isDeprecated 값을 통해 비활성화됩니다. 삭제 요청 시, 운동이 실행 중인지 확인하여 실행 중인 운동은 삭제할 수 없도록 제한합니다.
- 운동 시작
- 사용자가 운동을 시작할 수 있는 기능입니다.
- 사용자가 이미 다른 운동을 실행 중인 경우, 새로운 운동을 시작할 수 없습니다.
- 운동 시작 시, 현재 시간 기준으로 운동 시작 시간을 기록합니다. 운동이 시작되면 상태가
isActive = true
로 변경됩니다.
- 운동 종료
- 사용자가 진행 중인 운동을 종료하는 기능입니다.
- 한 번에 운동할 수 있는 최대 시간은 8시간 미만이며, 하루 총 운동 가능 시간은 12시간 미만입니다.
- 한 번에 8시간 이상 진행한 운동은 시간이 기록되지 않고 운동이 종료됩니다.
- 하루 총 운동 시간이 12시간을 초과하는 경우 11시간 59분 59초까지만 시간이 기록되고 운동이 종료됩니다.
- 운동 종료 시, 시작 시간부터 현재 시간까지의 운동 시간을 계산해
exerciseTime
에 저장하고,isActive = false
로 설정합니다.
- 운동 기록 저장
-
@Scheduled
의 cron을 이용하여 매일 새벽 3시에 사용자의 운동 기록을 히스토리에 저장하고, 하루 운동 시간을 초기화하는 스케줄러 기능이 있습니다. -
스케줄러가 실행되면 모든 진행 중인 운동을 강제 종료하고, 운동 시간을 저장한 후
ExerciseHistory
에ExerciseTime
의 기록을 이동시킵니다. 그리고 운동 시간을 기록한 사용자에 대해 출석일을 증가시킵니다.
-
- 일기 조회
- 메인 페이지에서 일기 조회가 가능하며 내림차순 정렬의 페이지네이션으로 구현되었습니다.
- 일기 생성
- 사용자가 일기를 작성할 수 있는 기능입니다. 사용자가 메모를 입력하여 생성 요청을 보내면 일기가 생성됩니다.
- 일기 삭제
- 일기는 아이디값을 기반으로 삭제가 이루어지며, 삭제는 hard delete 방식이 사용됩니다.
- 그룹 생성
- 사용자가 운동을 같이 할 그룹을 만드는 기능입니다. 팀 이름, 팀 설명, 패스워드, 적용할 태그리스트를 보내주면 이에 맞춰서 팀이 생성됩니다.
- 그룹 삭제
- 사용자가 만든 그룹을 삭제하는 기능입니다.
- 사용자는 여러개의 그룹에 가입이 가능하기에 N:M 관계가 성립됩니다. 따라서 그룹과 사용자간의 가입 정보를 나타내는
TeamMemberMapping
테이블이 존재하며, 그룹이 삭제되는 경우 해당 팀의TeamMemberMapping
데이터가 hard delete 됩니다. - 팀장만이 그룹을 삭제할 수 있으며, 그룹 자체와 그룹 내에서 팀원들끼리 주고받았던 채팅과 hard delete 방식으로 삭제됩니다.
- 그룹 조회
- 그룹 탐색 페이지에서 사용자가 현재 가입할 수 있는 그룹을 조회하는 기능입니다. 각 그룹마다 가지고 있는 Id값을 기준으로 오름차순으로 정렬되어 보여지며 페이지네이션으로 구현되었습니다.
- 사용자는 그룹 탐색 페이지에서 그룹 이름과 태그를 통해서 그룹을 필터링 할 수 있으며, 사용자가 적용한 필터링을 통해 해당되는 팀을 찾아 반환합니다.
- 팀 이름의 경우 해당 단어가 들어가는 팀은 모두 찾을 수 있습니다.
- 모든 그룹 태그 조회
- 저희 서비스에서 적용이 가능한 모든 그룹 태그들을 반환해주는 기능입니다.
- 기본적으로 관리자가 그룹 태그 삭제 시 soft delete 가 적용되어있어서, 해당 요청을 받으면 soft delete 되어있지 않은 그룹 태그들을 조회합니다. 그 후 각 그룹 태그들의 속성으로 묶어서 반환하게 됩니다.
- 랭킹 조회
- 사용자가 해당 팀에 속해있는 그룹원들간의 랭킹을 조회할 수 있는 기능입니다
- 요청으로 들어오는 날짜값에 맞춰서 해당 요일의 그룹원들의 운동시간을 기준으로 내림차순 으로 정렬되어 보여지며 페이지네이션으로 구현되었습니다.
- 또한 본인의 랭킹을 한눈에 찾을 수 있도록 별도로 본인의 랭킹도 응답으로 반환됩니다.
- 그룹 가입
- 사용자가 가입하고 싶은 그룹에 가입을 할 수 있는 기능입니다.
- 사용자가 가입하려는 팀이 최초로 가입하는 팀인 경우
TeamMemberMapping
테이블에 새로운 데이터가 추가됩니다 - 만약에 이전에 탈퇴했던 팀인 경우 soft delete 되어있던
TeamMemberMapping
의isDeprecated
값을false
로 바꾸어주는 방식으로 가입이 진행됩니다
- 그룹 탈퇴
- 사용자가 가입했던 그룹에서 탙퇴할 수 있는 기능입니다.
- 사용자가 그룹을 탈퇴 한 후에 재가입이 가능하므로
TeamMemberMapping
테이블에서 그룹과 사용자를 바탕으로 데이터를 찾은 후,TeamMemberMapping
데이터를 soft delete 하는 방식으로 그룹에서 탈퇴됩니다.
- 그룹 비밀번호 일치확인
- 사용자가 비공개 그룹에 가입 시 그룹에서 설정해 놓은 비밀번호와 일치한지 확인하는 기능입니다.
- 요청으로 들어온 비밀번호를 내부적으로 검사 후 일치한 경우는 Ok 응답을, 틀린 경우는 BadRequest 응답을 보내게 됩니다
- 내가 가입한 그룹 조회
- 나의 그룹 페이지에서 사용자가 가입한 그룹들을 보여ㅈ주는 기능입니다. 각 그룹마다 가지고 있는 Id값을 기준으로 오름차순으로 정렬되어 보여지며 페이지네이션으로 구현되었습니다.
- 채팅 조회
- 채팅방을 보기 전에 생성된 채팅 조회
- HTTP 프로토콜을 이용하며, 채팅 생성 시간 기준으로 내림차순 정렬의 페이지네이션으로 채팅 조회를 할 수 있습니다.
- 채팅방을 보는 중에 생성되는 채팅 조회
- WebSocket/Stomp 프로토콜을 이용하며, 채팅방에 연결하고, 해당 채팅방을 구독 했기 때문에 자신/상대방이 발행한(보낸) 채팅을 실시간으로 볼 수 있습니다.
- 채팅방을 보기 전에 생성된 채팅 조회
- 채팅 메세지 보내기
- WebSocket/Stomp 프로토콜을 이용하며, 채팅방에 연결하고, 해당 채팅방에서 채팅을 발행합니다.
- 채팅 기능은 이처럼 발행/구독 패턴을 따르며 해당 시퀀스 다이어그램 방식으로 작동됩니다.
- 상품 조회
- 사용자가 마켓의 상품을 조회하는 기능입니다.
- 마켓에서 원하는 태그를 기반으로 상품을 필터링할 수 있으며, 선택된 태그가 없을 경우 전체 상품 목록을 볼 수 있습니다.
- 모든 상품은 조회수 내림차순 및 가격 오름차순으로 정렬됩니다.
- 상품 조회수 증가
- 특정 상품을 선택하면 해당 상품의 조회수가 증가하여 인기도를 반영합니다.
@Modifying
과 커스텀 JPQL 쿼리를 사용하여 조회수 증가 연산을 원자적(atomic)으로 수행하여, 동시성 문제 없이 일관된 데이터를 유지할 수 있도록 했습니다.
-
상품 관리
- 상품의 CRUD 기능을 제공합니다.
- REST API 와 SSR endpoint를 분리하여 구현하였습니다. REST API를 이용하여 상품의 생성, 수정, 삭제와 같은 관리 작업을 수행하고, SSR endpoint를 통해 상품 관리 UI를 지원하며, 상품 목록 페이지, 추가/수정 폼을 제공합니다.
- 상품 등록
- 새로운 상품을 등록하며, 상품 태그는 필수 항목입니다.
- 상품 수정
- 상품의 이미지, URL, 이름, 가격 등의 정보와 태그를 수정할 수 있습니다.
- 상품 삭제
- 필요없는 상품은 삭제할 수 있습니다. 상품은 Soft Delete를 이용하여 처리하므로, 데이터의 추적이 가능합니다.
- 상품 조회
- 페이지네이션을 이용해 전체 상품 목록을 조회하며, 각 상품의 태그 정보를 함께 제공합니다.
-
태그 관리
- 태그는 상품 태그와 그룹 태그로 분류됩니다.
- REST API 와 SSR endpoint를 분리하여 구현하였습니다. REST API를 이용하여 태그의 생성, 삭제와 같은 관리 작업을 수행하고, SSR endpoint를 통해 태그 관리 UI를 지원하며, 태그 목록 페이지, 추가 폼을 제공합니다.
- 태그 조회
- 전체 태그 정보를 조회하며, 태그 목록 페이지를 통해 태그의 추가 및 삭제 작업을 수행할 수 있습니다.
- 태그 생성
- 태그별로 정보를 입력하여 생성 요청을 보내면 태그가 생성됩니다.
- 상품 태그: 이름
- 팀태그: 이름, 속성
- 태그별로 정보를 입력하여 생성 요청을 보내면 태그가 생성됩니다.
- 태그 삭제
- 필요 없는 태그는 삭제할 수 있으며, 삭제는 Soft Delete 방식으로 처리되어 데이터 추적이 가능합니다.
- 아래 화면은 모바일에서 실제 웹을 캡쳐한 사진입니다.
메인 페이지 | 나의그룹 페이지 | 그룹탐색 페이지 | 마켓 페이지 | 마이 페이지 |
---|---|---|---|---|
분류 | 기능1 | 기능2 | 기능3 | 기능4 | 기능5 |
---|---|---|---|---|---|
로그인 페이지 | 카카오 소셜 로그인 | ||||
메인 페이지 | 날짜별 총 운동시간, 상세운동기록, 일기 조회 | 운동 추가, 삭제 | 운동 시작, 종료 | 일기 작성, 삭제 | |
나의 그룹 페이지 | 사용자가 가입하거나 만든 전체 그룹 조회 | 사용자가 가입한 그룹과 만든 그룹에 대한 필터링 | 그룹원들의 날짜별 운동시간에 대한 랭킹 조회 | 그룹 탈퇴 | |
그룹 탐색 페이지 | 전체 그룹 조회 | 그룹 이름에 기반한 검색 | 그룹 태그를 이용한 필터링 | 비밀번호가 있으면 입력 후 그룹 가입, 없으면 바로 가입 | 그룹 생성 |
마켓 페이지 | 마켓 상품 조회 | 상품 태그를 이용한 필터링 | |||
마이 페이지 | 닉네임 변경 | 출석 일수, 운동 통계 조회 | 회원 탈퇴 | ||
관리자 로그인페이지 | 카카오 소셜 로그인 | ||||
관리자 상품 관리 페이지 | 상품 추가, 수정, 삭제 | ||||
관리자 상품 태그 관리 페이지 | 상품 태그 추가, 삭제 | ||||
관리자 팀 그룹 태그 관리 페이지 | 그룹 태그 추가, 삭제 |
폴더 구조 보기
src
├── main
│ ├── java
│ │ └── homeTry
│ │ ├── Application.java
│ │ ├── admin
│ │ │ ├── controller
│ │ │ │ ├── rest
│ │ │ │ │ └── AdminPageRestController.java
│ │ │ │ └── view
│ │ │ │ ├── AdminAuthorityController.java
│ │ │ │ ├── AdminMainPageController.java
│ │ │ │ └── AdminPageViewController.java
│ │ │ ├── dto
│ │ │ │ └── request
│ │ │ │ └── AdminCodeRequest.java
│ │ │ ├── exception
│ │ │ │ ├── AdminErrorType.java
│ │ │ │ └── badReqeustException
│ │ │ │ └── InvalidAdminCodeException.java
│ │ │ └── service
│ │ │ └── AdminPageService.java
│ │ ├── chatting
│ │ │ ├── config
│ │ │ │ └── ChattingConfig.java
│ │ │ ├── dto
│ │ │ │ ├── request
│ │ │ │ │ └── ChattingMessageRequest.java
│ │ │ │ └── response
│ │ │ │ └── ChattingMessageResponse.java
│ │ │ ├── endpointHandler
│ │ │ │ ├── async
│ │ │ │ │ └── ChattingMessageListener.java
│ │ │ │ └── rest
│ │ │ │ └── ChattingController.java
│ │ │ ├── exception
│ │ │ │ ├── ChattingErrorType.java
│ │ │ │ ├── badRequestException
│ │ │ │ │ ├── InactivatedMemberWithValidTokenException.java
│ │ │ │ │ ├── InvalidChattingTokenException.java
│ │ │ │ │ ├── InvalidTeamIdException.java
│ │ │ │ │ └── NoSuchMemberInDbWithValidTokenException.java
│ │ │ │ └── handler
│ │ │ │ ├── ChattingExceptionHandler.java
│ │ │ │ └── StompInterceptorErrorHandler.java
│ │ │ ├── interceptor
│ │ │ │ └── StompInterceptor.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ └── Chatting.java
│ │ │ │ └── vo
│ │ │ │ └── Message.java
│ │ │ ├── repository
│ │ │ │ └── ChattingRepository.java
│ │ │ └── service
│ │ │ └── ChattingService.java
│ │ ├── common
│ │ │ ├── annotation
│ │ │ │ ├── DateValid.java
│ │ │ │ ├── LoginMember.java
│ │ │ │ ├── PasswordValid.java
│ │ │ │ └── validator
│ │ │ │ ├── DateValidator.java
│ │ │ │ └── PasswordValidator.java
│ │ │ ├── auth
│ │ │ │ ├── LoginMemberArgumentResolver.java
│ │ │ │ ├── exception
│ │ │ │ │ ├── AuthErrorType.java
│ │ │ │ │ ├── badRequestException
│ │ │ │ │ │ ├── InvalidAuthCodeException.java
│ │ │ │ │ │ └── InvalidTokenException.java
│ │ │ │ │ └── internalServerException
│ │ │ │ │ ├── HomeTryServerException.java
│ │ │ │ │ └── KakaoAuthServerException.java
│ │ │ │ ├── jwt
│ │ │ │ │ ├── JwtAuth.java
│ │ │ │ │ └── JwtUtil.java
│ │ │ │ └── kakaoAuth
│ │ │ │ ├── client
│ │ │ │ │ └── KakaoApiClient.java
│ │ │ │ ├── config
│ │ │ │ │ ├── DevKakaoAuthConfigRegistrar.java
│ │ │ │ │ ├── KakaoAuthConfig.java
│ │ │ │ │ └── ProdKakaoAuthConfigRegistrar.java
│ │ │ │ ├── controller
│ │ │ │ │ ├── rest
│ │ │ │ │ │ └── KakaoAuthRestController.java
│ │ │ │ │ └── view
│ │ │ │ │ └── KakaoAuthSSRController.java
│ │ │ │ ├── dto
│ │ │ │ │ ├── KakaoMemberInfoDTO.java
│ │ │ │ │ ├── KakaoMemberWithdrawDTO.java
│ │ │ │ │ └── response
│ │ │ │ │ ├── KakaoErrorResponse.java
│ │ │ │ │ ├── KakaoMemberInfoResponse.java
│ │ │ │ │ └── TokenResponse.java
│ │ │ │ └── service
│ │ │ │ ├── KakaoAuthService.java
│ │ │ │ └── KakaoClientService.java
│ │ │ ├── config
│ │ │ │ ├── SwaggerConfig.java
│ │ │ │ └── WebMvcConfig.java
│ │ │ ├── constants
│ │ │ │ └── DateTimeUtil.java
│ │ │ ├── converter
│ │ │ │ └── DurationToLongConverter.java
│ │ │ ├── entity
│ │ │ │ ├── BaseEntity.java
│ │ │ │ └── SoftDeletableEntity.java
│ │ │ ├── exception
│ │ │ │ ├── BadRequestException.java
│ │ │ │ ├── CommonErrorType.java
│ │ │ │ ├── ErrorType.java
│ │ │ │ ├── InternalServerException.java
│ │ │ │ ├── dto
│ │ │ │ │ └── response
│ │ │ │ │ └── ErrorResponse.java
│ │ │ │ └── handler
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ └── interceptor
│ │ │ ├── AdminInterceptor.java
│ │ │ └── JwtInterceptor.java
│ │ ├── diary
│ │ │ ├── controller
│ │ │ │ └── DiaryController.java
│ │ │ ├── dto
│ │ │ │ ├── DiaryDto.java
│ │ │ │ └── request
│ │ │ │ └── DiaryRequest.java
│ │ │ ├── exception
│ │ │ │ ├── DiaryErrorType.java
│ │ │ │ └── badRequestException
│ │ │ │ └── DiaryNotFoundException.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ └── Diary.java
│ │ │ │ └── vo
│ │ │ │ └── Memo.java
│ │ │ ├── repository
│ │ │ │ └── DiaryRepository.java
│ │ │ └── service
│ │ │ └── DiaryService.java
│ │ ├── docs
│ │ │ └── SwaggerRedirectController.java
│ │ ├── exerciseList
│ │ │ ├── controller
│ │ │ │ └── ExerciseController.java
│ │ │ ├── dto
│ │ │ │ ├── request
│ │ │ │ │ └── ExerciseRequest.java
│ │ │ │ └── response
│ │ │ │ └── ExerciseResponse.java
│ │ │ ├── event
│ │ │ │ ├── ExerciseCreationEvent.java
│ │ │ │ ├── ExerciseCreationEventListener.java
│ │ │ │ └── ExerciseEventPublisher.java
│ │ │ ├── exception
│ │ │ │ ├── ExerciseErrorType.java
│ │ │ │ └── badRequestException
│ │ │ │ ├── ExerciseAlreadyStartedException.java
│ │ │ │ ├── ExerciseDeprecatedException.java
│ │ │ │ ├── ExerciseInProgressException.java
│ │ │ │ ├── ExerciseNotFoundException.java
│ │ │ │ ├── ExerciseNotStartedException.java
│ │ │ │ └── NoExercisePermissionException.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ ├── Exercise.java
│ │ │ │ │ ├── ExerciseHistory.java
│ │ │ │ │ └── ExerciseTime.java
│ │ │ │ └── vo
│ │ │ │ └── ExerciseName.java
│ │ │ ├── repository
│ │ │ │ ├── ExerciseHistoryRepository.java
│ │ │ │ ├── ExerciseRepository.java
│ │ │ │ └── ExerciseTimeRepository.java
│ │ │ └── service
│ │ │ ├── ExerciseHistoryService.java
│ │ │ ├── ExerciseSchedulerService.java
│ │ │ ├── ExerciseService.java
│ │ │ ├── ExerciseTimeHelper.java
│ │ │ └── ExerciseTimeService.java
│ │ ├── mainPage
│ │ │ ├── controller
│ │ │ │ └── MainPageController.java
│ │ │ ├── dto
│ │ │ │ └── response
│ │ │ │ └── MainPageResponse.java
│ │ │ └── service
│ │ │ └── MainPageService.java
│ │ ├── member
│ │ │ ├── controller
│ │ │ │ └── MemberController.java
│ │ │ ├── dto
│ │ │ │ ├── MemberDTO.java
│ │ │ │ ├── request
│ │ │ │ │ └── ChangeNicknameRequest.java
│ │ │ │ └── response
│ │ │ │ └── MyPageResponse.java
│ │ │ ├── exception
│ │ │ │ ├── MemberErrorType.java
│ │ │ │ ├── badRequestException
│ │ │ │ │ ├── InactivatedMemberException.java
│ │ │ │ │ ├── LoginFailedException.java
│ │ │ │ │ ├── MemberNotFoundException.java
│ │ │ │ │ └── RegisterEmailConflictException.java
│ │ │ │ └── internalServerException
│ │ │ │ └── UniqueKeyViolatonException.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ └── Member.java
│ │ │ │ ├── enums
│ │ │ │ │ └── Role.java
│ │ │ │ └── vo
│ │ │ │ ├── Email.java
│ │ │ │ └── Nickname.java
│ │ │ ├── repository
│ │ │ │ └── MemberRepository.java
│ │ │ ├── service
│ │ │ │ ├── MemberService.java
│ │ │ │ ├── MemberTeamWithdrawService.java
│ │ │ │ └── MemberWithdrawService.java
│ │ │ └── utils
│ │ │ └── RandomNicknameGenerator.java
│ │ ├── product
│ │ │ ├── controller
│ │ │ │ ├── rest
│ │ │ │ │ ├── AdminProductRestController.java
│ │ │ │ │ └── MarketController.java
│ │ │ │ └── view
│ │ │ │ └── AdminProductViewController.java
│ │ │ ├── dto
│ │ │ │ ├── request
│ │ │ │ │ └── ProductRequest.java
│ │ │ │ └── response
│ │ │ │ ├── ProductAdminResponse.java
│ │ │ │ └── ProductResponse.java
│ │ │ ├── exception
│ │ │ │ ├── ProductErrorType.java
│ │ │ │ └── badRequestException
│ │ │ │ ├── MissingProductTagException.java
│ │ │ │ └── ProductNotFoundException.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ ├── Product.java
│ │ │ │ │ └── ProductTagMapping.java
│ │ │ │ └── vo
│ │ │ │ ├── ProductImageUrl.java
│ │ │ │ ├── ProductName.java
│ │ │ │ ├── ProductPrice.java
│ │ │ │ ├── ProductUrl.java
│ │ │ │ └── StoreName.java
│ │ │ ├── repository
│ │ │ │ ├── ProductRepository.java
│ │ │ │ └── ProductTagMappingRepository.java
│ │ │ └── service
│ │ │ ├── AdminProductService.java
│ │ │ ├── ProductService.java
│ │ │ └── ProductTagMappingService.java
│ │ ├── tag
│ │ │ ├── controller
│ │ │ │ ├── rest
│ │ │ │ │ └── TagRestController.java
│ │ │ │ └── view
│ │ │ │ └── AdminTagViewController.java
│ │ │ ├── exception
│ │ │ │ ├── TagErrorType.java
│ │ │ │ └── badRequestException
│ │ │ │ └── ForbiddenTagAccessException.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ └── Tag.java
│ │ │ │ └── vo
│ │ │ │ └── TagName.java
│ │ │ ├── productTag
│ │ │ │ ├── dto
│ │ │ │ │ ├── ProductTagDto.java
│ │ │ │ │ ├── request
│ │ │ │ │ │ └── ProductTagRequest.java
│ │ │ │ │ └── response
│ │ │ │ │ └── ProductTagResponse.java
│ │ │ │ ├── exception
│ │ │ │ │ ├── ProductTagErrorType.java
│ │ │ │ │ └── badRequestException
│ │ │ │ │ ├── ProductTagAlreadyExistsException.java
│ │ │ │ │ └── ProductTagNotFoundException.java
│ │ │ │ ├── model
│ │ │ │ │ └── entity
│ │ │ │ │ └── ProductTag.java
│ │ │ │ ├── repository
│ │ │ │ │ └── ProductTagRepository.java
│ │ │ │ └── service
│ │ │ │ └── ProductTagService.java
│ │ │ └── teamTag
│ │ │ ├── dto
│ │ │ │ ├── AllTeamTagDTO.java
│ │ │ │ ├── TeamTagDTO.java
│ │ │ │ ├── request
│ │ │ │ │ └── TeamTagRequest.java
│ │ │ │ └── response
│ │ │ │ └── TeamTagResponse.java
│ │ │ ├── exception
│ │ │ │ ├── TeamTagErrorType.java
│ │ │ │ └── badRequestException
│ │ │ │ ├── TeamTagAlreadyExistsException.java
│ │ │ │ └── TeamTagNotFoundException.java
│ │ │ ├── model
│ │ │ │ ├── entity
│ │ │ │ │ └── TeamTag.java
│ │ │ │ └── vo
│ │ │ │ └── TeamTagAttribute.java
│ │ │ ├── repository
│ │ │ │ └── TeamTagRepository.java
│ │ │ └── service
│ │ │ └── TeamTagService.java
│ │ └── team
│ │ ├── controller
│ │ │ └── TeamController.java
│ │ ├── dto
│ │ │ ├── RankingDTO.java
│ │ │ ├── request
│ │ │ │ ├── CheckingPasswordRequest.java
│ │ │ │ └── TeamCreateRequest.java
│ │ │ └── response
│ │ │ ├── RankingResponse.java
│ │ │ ├── TagListResponse.java
│ │ │ └── TeamResponse.java
│ │ ├── exception
│ │ │ ├── TeamErrorType.java
│ │ │ └── badRequestException
│ │ │ ├── AlreadyJoinedTeamException.java
│ │ │ ├── InvalidPasswordException.java
│ │ │ ├── MyRankingNotFoundException.java
│ │ │ ├── NotTeamLeaderException.java
│ │ │ ├── TeamHasNotPasswordException.java
│ │ │ ├── TeamLeaderCannotWithdrawException.java
│ │ │ ├── TeamMemberNotFoundException.java
│ │ │ ├── TeamNameAlreadyExistsException.java
│ │ │ ├── TeamNotFoundException.java
│ │ │ └── TeamParticipantsFullException.java
│ │ ├── model
│ │ │ ├── entity
│ │ │ │ ├── Team.java
│ │ │ │ ├── TeamMemberMapping.java
│ │ │ │ └── TeamTagMapping.java
│ │ │ └── vo
│ │ │ ├── Description.java
│ │ │ ├── Name.java
│ │ │ ├── Participant.java
│ │ │ └── Password.java
│ │ ├── repository
│ │ │ ├── TeamMemberMappingRepository.java
│ │ │ ├── TeamRepository.java
│ │ │ └── TeamTagMappingRepository.java
│ │ └── service
│ │ ├── TeamJoinAndWithdrawService.java
│ │ ├── TeamMemberMappingService.java
│ │ ├── TeamService.java
│ │ └── TeamTagMappingService.java
│ └── resources
│ ├── application-dev-kakao-login.properties
│ ├── application-dev.properties
│ ├── application-prod-kakao-login.properties
│ ├── application-prod.properties
│ ├── application-secret.properties
│ ├── application.properties
│ ├── data-dev.sql
│ ├── data.sql
│ ├── static
│ │ ├── css
│ │ │ ├── admin
│ │ │ │ ├── adminAuthority.css
│ │ │ │ ├── adminMainPage.css
│ │ │ │ └── adminPromote.css
│ │ │ ├── product
│ │ │ │ ├── productAdd.css
│ │ │ │ ├── productEdit.css
│ │ │ │ └── productList.css
│ │ │ ├── styles.css
│ │ │ └── tag
│ │ │ ├── addProductTag.css
│ │ │ ├── addTeamTag.css
│ │ │ ├── productTags.css
│ │ │ └── teamTags.css
│ │ ├── image
│ │ │ ├── add.png
│ │ │ ├── edit.png
│ │ │ ├── kakao_login_medium_narrow.png
│ │ │ ├── previous.png
│ │ │ ├── remove.png
│ │ │ └── save.png
│ │ └── js
│ │ ├── admin
│ │ │ ├── adminAuthority.js
│ │ │ ├── adminMainMage.js
│ │ │ └── adminPromote.js
│ │ ├── main.js
│ │ ├── product
│ │ │ ├── productAdd.js
│ │ │ ├── productEdit.js
│ │ │ └── productList.js
│ │ └── tag
│ │ ├── productTagAdd.js
│ │ ├── productTagDelete.js
│ │ ├── teamTagAdd.js
│ │ └── teamTagDelete.js
│ └── templates
│ ├── admin
│ │ ├── adminAuthority.html
│ │ ├── adminKakaoLoginResult.html
│ │ ├── adminMain.html
│ │ ├── adminPage.html
│ │ └── adminPromote.html
│ ├── product
│ │ ├── productAdd.html
│ │ ├── productEdit.html
│ │ └── productList.html
│ └── tag
│ ├── addProductTag.html
│ ├── addTeamTag.html
│ ├── productTags.html
│ └── teamTags.html
└── test
└── java
└── homeTry
├── chatting
│ └── ChattingTest.java
├── common
│ └── config
│ └── CorsTest.java
├── diary
│ └── DiaryTest.java
├── exerciseList
│ ├── ExerciseTest.java
│ └── service
│ ├── ExerciseSchedulerServiceTest.java
│ └── ExerciseTimeServiceTest.java
├── mainPage
│ └── MainPageTest.java
├── member
│ └── MemberTest.java
├── product
│ ├── AdminProductViewTest.java
│ └── MarketTest.java
├── tag
│ └── TagPageTest.java
└── team
└── TeamTest.java