From 7ccec30b2ca0c8b0ce29df9806f4ad0dc8e3d0cb Mon Sep 17 00:00:00 2001 From: Dora Choo Date: Fri, 11 Oct 2024 22:15:26 +0900 Subject: [PATCH] v1.1.4 (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 댓글방 디테일 화면 구현 (#32) * feat: font 설정 * feat: vector 이미지 추가 * feat: 채팅 아이템 뷰 구현 * refactor: 컨벤션에 맞게 네이밍 수정 * feat: 댓글 입력 edit text 구현 * chore: 백엔드 CD 스크립트 작성 (#34) * chore: 백엔드 CD 스크립트 작성 * chore: 도커 백그라운드로 실행 * chore: 도커 설정 및 트리거 설정 변경 * chore: 도커 이미지 제거 로직 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 강제 제거하도록 수정 * chore: gradle 캐싱 로직 추가 (#39) * chore: gradle 캐싱 로직 추가 * chore: 이벤트 트리거 조건 수정 * feat: 공모 참여하기 기능 구현 (#40) * fix: BaseTimeEntity 적용 오류 수정 Co-authored-by: Dora Choo * feat: 참여하기 API 구현 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: 공모 상세 조회 API에 참여자 목록 필드 추가 (#42) * feat: 공모 상세 조회 API의 request에 memberId 필드 추가 (#45) * feat: 공모 참여 API의 불필요한 응답값 전부 제거 (#48) * feat: 공모 참여 API의 불필요한 반환값 제거 * chore: 자주 쓰는 h2 console enabled 설정 주석 처리 * feat: 이미 참여한 공모에 참여 못하게 예외 처리 (#51) * feat: 공모 상세 페이지 API 연결 (#46) * build: 불필요한 의존성 제거, properties관련 코드 작성 * refactor: base_url코드상에서 제거 * feat: api수정에 따른 필드 변경 및 네이밍 반영 * refactor: 네이밍 변경 * refactor: OfferingDetail의 변경, mapper변경 * refactor: service분리 * refactor: DataSource, Repository분리 * refactor: API변경에 따른 리팩토링 * feat: 공모 상세 조회 기능 구현 * refactor: 참여하기 api변경에 따른 data, domain 코드 수정 * feat: 공모 상세 페이지 참여하기 기능 구현 * feat: 공모 상세 화면에서 이미지를 불러올 수 없을 시 기본이미지를 보여주는 기능 구현 * feat: 게시물 상세 화면 폰트 적용 * style: lint적용 * refactor: 액티비티 destroy시 binding해제하도록 코드 추가 * refactor: glide옵션 변경 - 에러 발생 시 보여줄 이미지 - url이 null일 시 보여줄 이미지 * refactor: viewModel에 custom getter추가 * fix: 내용이 짧을 시 뒷 배경이 회색으로 보이는 버그 수정 * fix: 참여하기 버튼을 눌렀을 시 텍스트가 바뀌지 않는 버그 수정 * feat: 테스트 데이터 다양화 (#52) Co-authored-by: Dora Choo * refactor: 공모 엔티티에 currentCount 필드 추가 (#55) * feat: 댓글 작성 API 구현 (#57) * feat: 댓글방 내 공모 일정 조회 기능 구현 (#58) * feat: 댓글방 내 공모 일정 조회 기능 구현 Co-authored-by: Dora Choo * refactor: 공모 일정 조회 api 명세 변경 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * refactor: common 패키지명을 global로 변경 (#61) * chore: 안드로이드 CI 파일 작성 (#63) * feat: 댓글 목록 조회 API 구현 (#66) * chore: build CI 작업을 위한 manifest 파일 수정 (#65) * chore: 알람 권한 추가 * chore: local properties 속성 추가 * chore: local properties null 체크 로직 추가 * chore: buildConfigField null 체크 * style: lint 적용 * chore: secret 값 설정 * fix: secret 값 오류 수정 * fix: 문법 오류 수정 * chore: 경로 수정 * chore: 문법 수정 * style: lint 적용 * feat: 댓글방 목록 조회 API 구현 (#70) * feat 댓글방 접히는 공지 뷰 구현 (#72) * chore: manifest에 CommentDetailActivity 추가 * feat: BindingAdatper을 사용하여 접힐 때 애니메이션 적용 및 픽셀 변환 * feat: viewmodel 구현 및 click 마다 접히고 펴지는 로직 구현 * style: ktlint 적용 * refactor: binding adpater을 사용하여 가시성 변경 * refactor: 댓글방 및 댓글 목록 조회 서비스 계층 (#78) * fix: 댓글방 목록 조회 시 가장 최근 댓글 조회 (#80) * feat: 홈화면 API 연결 (#74) * refactor: API변경에 따른 data, domain 코드 변경 * feat: 공모 목록 기능 구현 * refactor: 함수 분리 * style: lint적용 * style: font 적용 * fix: 시간순 정렬 쿼리 추가 (#83) * chore: 더미 데이터 추가 (#87) * feat: 댓글방 목록 API 연결 (#82) * feat: bottom navigation fragment 추가 * feat: vector 이미지 추가 * feat: 댓글방이 없으면 "채팅 목록이 없어요" 라는 텍스트뷰와 이미지뷰를 띄우는 기능 구현 * feat: 댓글방 띄우는 기능 구현 * test: 댓글방 UI 테스트 작성 * refactor: 테스트 클래스명 수정 * refactor: 줄바꿈 수정 * feat: 댓글방 API 서비스 구현 * refactor: API 명세에 따라 도메인 모델 수정 * feat: API 연결 * refactor: API명세에 따라 데이터바인딩 변수명 수정 * feat: 댓글방 목록 API 연결 * refactor: ktlint Format 적용 * refactor: 메모리 누수 방지를 위해 fragment가 destroy 될 때 _binding을 null로 설정 * refactor: 어답터를 방어적복사 하지 않아도 되어서 수정 * refactor: 채팅방이 없다는 이미지뷰를 띄워주는 방식 수정(바인딩 어댑터 수정) * refactor: 함수분리 * refactor: ktFormat 적용 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> * feat: 댓글방 접히는 공지 API 연결 (#85) * feat: 미팅 일정 API 연결을 위한 data layer 구현 * feat: 미팅 일정 API 연결을 위한 domain layer 구현 * feat: 미팅 일정 API 연결을 위한 presentation layer 구현 * style: ktlint 적용 * feat: 공동 구매 제목 databinding 적용 * refactor: 변수명 수정 * fix: 펼치기 접기 버튼 로직 반대로 수정 * style: ktlint 적용 * chore: 더미 데이터 바로가기 url 수정 (#93) * feat: 공모 상세 페이지 기능 추가 (#94) * chore: 마이페이지 닉네임 임시로 지정 * feat: 바로가기 기능 구현 * feat: 참여버튼 클릭 시 댓글방으로 가도록 기능 구현 * feat: 신고하기 이미지 추가 * style: lint적용 * refactor: 불러오는 공모 페이지 사이즈 변경 * refactor: 댓글 도메인 코드 리팩터링 (#96) * refactor: 로그인 멤버 변수명 변경 * refactor: JPQL 쿼리 컨벤션 및 멤버로 공모 조회 메서드명 변경 * refactor: 최근 댓글 응답 클래스명 변경 * refactor: 컨트롤러 및 서비스 API 순서 변경 * refactor: 로그인 사용자 유효성 검증 * feat: 댓글방 댓글 작성 api 연결 (#95) * chore: windowSoftInputMode 추가 * feat: post comment api service 구현 * feat: post comment DataSource 구현 * feat: post comment Repository 구현 * feat: post comment Presentation 구현 * chore: 더미 데이터 시간 변경 (#100) * feat: 댓글방 입장 기능, 본인이 총대인 방은 다르게 보이는 기능 구현 (#99) * feat: 댓글방의 마지막 댓글 시간을 띄우는 기능 구현 * feat: 자신이 총대인 댓글방을 표시하는 기능 구현 * feat: 댓글방 목록을 클릭해 댓글방 상세로 이동하는 기능 구현 * test: UI테스트 수정 * refactor: 클릭시 id 뿐만 아니라 title도 받아오는 방식으로 수정 * refactor: 오전/오후와 시간을 텍스트뷰에 띄우는 바인딩 어댑터를 DateTimeFormatter의 기능을 사용하는 것으로 수정 * refactor: memberId를 local.properties의 token을 가져다 쓰는 것으로 변경(임시 조치) * refactor: 댓글방 목록의 시간을 띄우는 바인딩 어댑터의 속성명을 수정함 * refactor: 데이터바인딩 variable 변수명을 구체적으로 수정, 일관성을 위해 앞에 `on` 붙임 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정(빠트린것 수정함) * feat: 전반적인 예외 처리 (#103) * feat: 예외 처리 핸들러 추가 * feat: Offering 예외 처리 코드 추가 * feat: Comment 예외 처리 코드 추가 * feat: Member 예외 처리 코드 추가 * feat: OfferingMember 예외 처리 코드 추가 * feat: Offering 예외 처리 상세 코드 추가 * feat: 에러 코드 적용 * feat: 도메인 검증 로직 * feat: DTO 검증 로직 --------- Co-authored-by: masonkimseoul * feat: swagger와 restdocs 연동 (#104) * chore: swagger ui 정적 파일 설치 및 static routing 세팅 * chore: restdocs-api-spec을 이용한 OAS 생성 * chore: swagger ui 정적 파일을 swagger-ui 디렉토리로 이동 * chore: swagger ui 정적 파일 및 static routing 세팅 제거 * chore: 생성된 OAS 파일을 Swagger 디렉터리로 복사하는 스크립트 작성 * chore: openapi3 yaml 파일 gitignore 처리 * chore: static routing 세팅 다시 추가 openapi3.yaml을 사용하기 위함 * test: RestAssured RestDocs 테스트 코드 작성 * test: 공모 목록 조회 API 문서화 * test: 공모 일정 조회 API 및 공모 참여 API 문서화 * test: 댓글 관련 API 문서화 * docs: 논의된 TODO 제거 * refactor: swagger 어노테이션 제거 * chore: 개발 API 서버 목록 설정 --------- Co-authored-by: fromitive * refactor: 에러메시지 필드명 변경 (#108) * fix: restdocs 관련 테스트 실패 이슈 해결 (#106) * chore: cicd 테스트 * chore: 테스트 위해 actions 범위 조정 * chore: 배포 스크립트 띄어쓰기 오타 수정 * chore: 빌드 캐싱 제거 * chore: logging * chore: --warning-mode all 옵션 줘서 gradle 호환 무시하도록 설정 * fix: status 달라서 실패하는 테스트 수정 * chore: actions 범위 수정 * chore: action 범위 수정 * chore: test용 static 파일 추가 * chore: static 하위 폴더를 jar 파일에 포함하도록 설정 * chore: swagger-ui 하위 폴더 제거 * chore: task 순서 조정 * chore: build 스크립트 수정 * chore: 불필요한 설정 변경 제거 * chore: clean build 대신 clean bootJar 사용 * chore: clean, build 각각 하도록 변경 * chore: test 까지 두 번 돌리도록 수정 * chore: openapi3까지 두 번 실행하도록 수정 * chore: copyOasToSwagger 까지 두번 실행하도록 수정 * chore: actions 활성화 범위 수정 * fix: 댓글방 목록 조회 시 참여자 수 조건 추가 (#111) * fix: 댓글방 조회 테스트 수정 (#113) * feat: 홈 화면 무한 스크롤 기능 구현 (#109) * build: pagination라이브러리 추가 * feat: 홈 화면 무한 스크롤 기능 구현 * fix: 마지막 댓글 response를 nullable하게 수정 (#115) * fix: 마지막 댓글 response를 nullable하게 수정 * refactor: ktFormat 적용 * feat: 댓글방 댓글 조회 api 연결 (#116) * feat: dto 및 mapper 구현 * feat: 댓글방 목록 service 구현 * feat: 댓글방 목록 data source 구현 * feat: 댓글방 목록 repository 및 model 구현 * feat: 댓글방 목록 view type을 활용한 recyclerview 구현 및 데이터 바인딩 * feat: polling 기능 구현 * feat: 댓글 스크롤 구현 (새로운 댓글이 생길시 스크롤 아래로) * feat: 총대와 다른 참가자 이미지 리소스 파일 * feat: 댓글방 디테일 공동 구매 상태별 관리 (#117) * feat: 공동구매 상태 관리 리소스 파일 * feat: 공동구매 상태를 관리하는 enum class 구현 * feat: 데이터바인딩을 사용하여 공동 구매 상태 뷰 업데이트 구현 * style: ktlint 적용 * feat: 공동구매 상태 관리 리소스 파일 추가 * fix: 이미지 링크 임시 수정 (#119) * fix: 이미지 링크 수정 (#120) * refactor: 네이밍 수정 (#123) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 (#125) * refactor: 뷰모델 팩토리 방식 변경 (#130) * refactor: 뷰모델 팩토리를 뷰모델의 동반객체로 이동 * style: lint적용 * refactor: Service분리 (#132) * refactor: service분리 * refactor: 패키지명 변경 * style: lint적용 * feat: 공모글 작성 UI 구현 (#134) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 뷰 구현 * fix: 뷰 수정사항 반영 * fix: @+id로 참조하는 부분을 수정 * fix: drawable의 네이밍에 where을 추가 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 (#136) * feat: 참여자 목록 drawer에 필요한 리소스 파일 추가 * refactor: 채팅 text gravity 수정 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 * style: ktlint 적용 * refactor: drawer early return 하는 방식으로 변경 * refactor: ivMore -> ivMoreOptions으로 네이밍 변경 * feat: 공구 참여자 item view 및 댓글방 view 사용자 친화적으로 수정 * chore: CI 빌드 스크립트 중 중복되는 task 제거해 성능 개선 (#128) * chore: jar태스크 비활성화하고 bootJar 태스크로만 JAR 파일 생성 * chore: cicd 범위 조정 * feat: 공모 작성 API 구현 (#139) * feat: 공모 작성 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: create를 save로 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: dto entity 매핑로직을 dto로 이동 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: controller request 매개변수 명 컨벤션 적용 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 공모에 저장하는 주소 값 구체화 (#141) * refactor: 공모에 저장하는 주소 값 구체화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: github-action 스크립트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: CI/CD test 설정 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: static/swagger-ui 폴더 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: 설정 원상 복구 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: ci/cd 범위 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 홈화면(공모목록) UI 추가 구현 및 상태 변경 대응 (#142) * feat: 공모의 상태 변경이 반영되도록 기능 구현 * feat: 공모 목록 ui변경 * feat: 필터 ui추가 * feat: API변경에 따른 DTO수정 * style: lint적용 * feat: resource추가 * refactor: ui위치 수정 * chore: 불필요한 괄호 제거 * refactor: item 수직 정렬 * feat: 댓글방 메시지 조회 시 commentId 필드 추가 (#150) Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: OG 태그 크롤링 API 구현 (#148) * feat: OG 태그 크롤링 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: OG 태그 크롤링 API 엔드포인트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 제품 코드와 API 문서 동기화 (#153) * refactor: API 문서 개선 (#157) * refactor: 댓글 작성 시 성공 상태 코드 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 요청 필수 상태 설명 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: s3 이미지 업로드 API 구현 (#147) * feat: s3 이미지 업로드 API 구현 * chore: cicd 액션 범위 수정 * fix: 이미지 업로드 경로의 특수문자 제거 * chore: yml multipart 설정 추가 * chore: S3 업로드 결과 테스트 * fix: inputstream 변환로직 위치 이동 * fix: 업로드할 s3 path 올바르게 수정 * fix: 사진 url 속에 버킷이름을 cloudfront 도메인으로 수정 * chore: actions 범위 재조정 * feat: API endpoint 변경 * chore: docker image 지우는 작업을 마지막으로 이동 * chore: 다른 브랜치로 이전 커밋 이동하기 위해 제거 * chore: 충돌 해결 및 코드 스타일 변경 * test: S3 이미지 업로드 성공 케이스 추가 * test: multipart form data 문서화 * test: 공모 상태 enum 문서화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 파일 업로드 크기 제한 100MB에서 20MB로 변경 --------- Co-authored-by: Choo Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 주소검색 기능구현 (#161) * refactor: 네이밍 컨벤션 적용 * build: webview 라이브러리 추가 * feat: 스크립트 실행위한 html파일 추가 * refactor: 인터페이스명 변경에 따른 변경 * feat: 주소검색 다이얼로그 레이아웃 작성 * feat: 주소검색 기능 구현 * style: lint적용 * refactor: 불필요한 코드 제거 * build: Firebase의존성 추가 (#165) * feat: 공모글 작성 API 연결 (#162) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 API 연결 구현 * feat: 공모글 작성 뷰모델 구현 * fix: edit text 데이터바인딩 추가 * chore: 테스트를 위해 MutableLiveData default값 넣어둠 * chore: deadline defualt값 형식에 맞게 수정 * feat: 글작성 화면을 액티비티에서 프래그먼트로 수정 * chore: 테스트목적이었던 주석과 mutable livedata 디폴트값 제거 * refactor: 임시 함수명 수정 * fix: 글작성 프래그먼트가 올라오기 전에 바텀 네비게이션이 사라지는 문제 수정 * feat: 필수 항목이 모두 입력되어야 버튼이 활성화 되는 기능 구현 * feat: 가격, 총원 입력이 잘못되었을 시 토스트를 띄우는 기능 구현 * fix: 버튼 비활성화 시 텍스트 변경 * feat: 앱 아이콘 변경 * feat: 앱 이름 변경(chongdae -> 총대마켓) * feat: 예상 엔빵 가격을 보여주는 기능 구현 * refactor: 상수화 * refactor: 예상 엔빵 가격에 ,가 들어가는 기능 구현, 콜론 뒤 white space 추가 * feat: 공구 할인율을 계산해 주는 기능 구현 * feat: +, - 버튼으로 총원을 조절하는 기능 구현 * fix: 할인율과 엔빵가격 계산 시 0으로 나눠지는 상황을 제거 * fix: 맞춤법 수정 할인률 -> 할인율 * fix: 총원 버튼 크기가 너무 작아서 확대 * fix: 항목간 간격이 좁아서 확대 * refactor: Offering Write의 API service, DataSource, Repository를 Offerings와 합침 * refactor: 디버깅용 코드 삭제 * refactor: 버튼 활성화/비활성화를 selector와 삼항연산자로 구현 * refactor: 바인딩어댑터 대신 뷰모델이 visibility 상태를 갖고 있는 방식으로 변경 * refactor: 바인딩어댑터 대신 xml에서 처리하는 방식으로 변경 * refactor: 총원 디폴트 라이브데이터값 상수화 * refactor: +, - 텍스트뷰 버튼으로 수정 * refactor: textStyle bold대신 fontFamily suit_bold를 쓰는 것으로 수정 * refactor: 변수명 뒤에 Int를 붙이는 것 대신 Value를 붙이는 것으로 수정 * refactor: 글작성 제출 버튼의 아이디를 추가 * refactor: ktFormat * refactor: 토스트를 띄우는 함수 분리 * refactor: 도메인 객체 분리 * refactor: UI모델 적용 * refactor: ktFormat 적용 * feat: 댓글방 디테일 Room을 사용하여 data 저장 (#166) * feat: local database 구현 * feat: entity 구현 * feat: dao 구현 * feat: LocalDataSourceImpl 구현 * feat: entity mapper 구현 * refactor: CommentResponse 에 id 값 추가 * refactor: datasource 이름 변경 및 패키지 변경 * refactor: article -> offering으로 네이밍 변경 * refactor: repository 패키지 변경에 따른 수정 * refactor: datasource 패키지 변경 및 local 과 remote 분리 * refactor: repository Application 클래스를 통한 주입으로 변경 * style: ktlint 적용 * refactor: api service 리네이밍 * refactor: git conflict 해결 * refactor: 함수 이름 컨벤션에 맞도록 변경 (getMeetings -> fetchMeetings) * chore: CI 스크립트 추가 (#173) * chore: ci 스크립트 추가 * chore: ci 스크립트 수정 * fix: og 태그 추출 시 크롤링 이슈 해결 (#174) * feat: 날짜, 시간 선택 기능 구현, 주소검색 기능 연결 (#171) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 모집마감 시간 클릭 시 date time picker를 띄우는 기능 구현 * feat: 날짜, 시간 선택 기능 구현 * feat: 주소 검색 기능 연결 * refactor: 함수명 수정, 함수분리 * refactor: ktFormat 적용 * refactor: string으로 분리, 상수화 * fix: string 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정3 * chore: CI workflow 파일 수정4 * feat: 공모가 정상적으로 게시되었을 시 "공모가 게시되었어요!" 라는 토스트를 띄우고 공모글 작성 프래그먼트를 종료하는 기능 구현 * feat: 토스트가 화면 중앙에 뜨는 문제 수정 * refactor: 사용되지 않는 파일 삭제 * refactor: xml 뷰 id 수정 * refactor: 버튼이 TextView인 문제 수정 * refactor: 사용되지 않는 data binding variable 제거 * refactor: 함수명 수정 * refactor: 다이얼로그, dateTimePickerBinding 전역으로 선언 * refactor: dateTimePicker 클릭 이벤트를 추상화 해 xml에서 처리하도록 변경 * refactor: ktFormat * feat: 상품 URL 이미지 추출 API 연결 (#180) * refactor: 사용하지 않는 파일 제거 * refactor: 가시성 변경 * feat: api service 구현 * feat: datasource 구현 * refactor: repository 네이밍 수정 (offeringsRepository -> offeringRepository) * feat: 사진 업로드 관련 리소스 파일 추가 * feat: repository 및 model 구현 * feat: 이미지 링크를 통한 크롤링 이미지 불러오는 api 연결 및 이미지 삭제 로직 구현 * style: ktlint 적용 * refactor: 이미지 prefix 추가 및 에러 메시지 수정 * refactor: build 오류 수정 * fix: git conflict 해결 * feat: 공모 목록 조회 API에 필터링과 검색 기능 추가 (#169) * feat: 공모 필터 목록 조회 API 구현 * test: 공모 필터 목록 조회 API 테스트 * style: 개행 형식 통일 * feat: 공모 필터 목록 조회 API Specification 도입 준비 * fix: url에 큰따움표 제거 * feat: Specification 도입 * refactor: queryString 구체화 * refactor: 함수명 변경 * feat: 최신순 필터링 적용 * feat: 마감임박순 필터링 적용 * feat: 높은할인률순 필터링 적용 * refactor: 전략 패턴 적용해 여러 갈래의 분기문과 중복되는 코드 처리 * test: 변경된 API 스펙에 맞게 문서화 작업 * refactor: 관련있는 메서드들끼리 모이게 순서 재배치 * refactor: 맞춤법 수정 * style: 개행 제거 --------- Co-authored-by: masonkimseoul * feat: 상태 변경 API 구현 (#175) * feat: 댓글방 상태 변경 및 조회 API 구현 Co-authored-by: masonkimseoul * feat: 공모글 상태 조회 API 구현 * feat: 댓글방 상태 변경 중 수동 확정 기능 구현 * refactor: 상태 변경 관련 메서드명 수정 * refactor: 추상 클래스 메서드 컨벤션 통일 * refactor: errorCode 사용 시 클래스 명시 * refactor: 댓글방 상태 관련 API 엔드포인트 수정 및 패키지 변경 * refactor: 댓글방 상태 변경 API HTTP 메서드 수정 * feat: 공모 모집 자동 확정 시 댓글방 상태 변경 --------- Co-authored-by: masonkimseoul Co-authored-by: Choo * feat: 로그인 기능 구현 (#177) * feat: password 일방향 암호화 기능 구현 * feat: cookie 생산-소비 기능 구현 * chore: jwt 관련 의존성 추가 * feat: 토큰 생성 기능 구현 * feat: 로그인 API 구현 * test: 로그인 API 테스트 * feat: 회원가입 API 구현 * test: 회원가입 API 테스트 * feat: 닉네임 생성 기능 구현 * test: 닉네임 생성 기능 테스트 * fix: postconstruct 여러 개라 발생한 에러 해결 * feat: 회원가입 응답값에 랜덤생성한 닉네임 추가 * feat: MemberArgumentResolver 구현 * feat: MemberArgumentResolver 일부 적용 * test: 바뀐 스펙에 맞게 변경 * test: TestConfig 설정해 빈충돌 오류 해결 * test: 공모 작성 API로 MemberArgumentResolver 사용 * feat: 토큰 재발급 API 구현 * test: 토큰 재발급 API 테스트 * test: 토큰 재발급 API 에러 테스트 * feat: MemberArgumentResolver commant에 적용 * feat: MemberArgumentResolver offering에 적용 * feat: MemberArgumentResolver participant에 적용 * refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경 * refactor: 클래스명 일관적으로 변경 * refactor: 직관적인 명명으로 enum 네이밍 변경 * refactor: Custom Exception 적용 * refactor: 컨트롤러 메서드에 접근제어자 명시 * fix: 중복된 enum 값 제거 * test: 바뀐 API 스펙에 맞게 변경 --------- Co-authored-by: fromitive * fix: nicknameWordInitializer 설정 오류 해결 (#182) * fix: keyword null일 때 처리 및 docs에서 required 제거 (#184) * fix: keyword null일 때 처리 * test: optional() 붙여서 required 제거 * chore: 브랜치에 상관없이 pr 머지 시 자동으로 관련 이슈 닫는 스크립트 구현 (#187) * fix: og 이미지 태그 크롤링 문제 해결 (#190) * refactor: 댓글방 상태 도메인 설계 변경 (#189) * feat: 공모 목록 API 응답값에 낱개 가격 추가 (#193) * chore: readtimeout 5초로 수정 (#195) * feat: 댓글방 상태 조회 시 상태별 이미지 함께 반환 (#196) * feat: 공모 목록 조회 API연결 (#201) * refactor: Condition 수정에 따른 변경 * refactor: api변경에 따른 리팩토링 * refactor: api변경에 따른 목록 무한 스크롤 기능 리팩토링 * feat: 검색 기능 구현 * feat: 필터링 기능 구현 - 참여 가능은 서버 에러로 추후 추가 예정 * feat: 아이템을 불러온 후 recyclerview의 최상단으로 이동하는 기능 구현 - 검색, 필터링 수행 후 최상단으로 이동 * feat: 필터링 목록 불러오는 api연결 * feat: 마감임박 상태 추가 * refactor: default parameter제거 * style: lint적용 * feat: 토큰 반환 시 cookie가 아닌 body 사용하도록 변경 (#206) * feat: 발급한 토큰을 header가 아닌 body로 반환하도록 수정 * refactor: 사용안하는 클래스와 메서드 제거 * test: 바뀐 API 스펙에 맞게 명세 수정 * feat: 이미지 더미 데이터 수정 및 부정확한 가격 데이터 수정 (#207) * refactor: 공모 글 작성 시 총대 참여자 추가 (#208) * feat: 바텀 네비게이션 고정 기능 구현 (#211) * feat: 데이터에서 5자 이상 제거 (#212) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 (#202) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 * refactor: 도메인 명칭 변경 (낱개가격 -> 원가격) * refactor: 도메인 명칭 변경 (공모 -> 댓글방) * refactor: originPrice로 http client 변경 * feat: 키보드 이외 영역 터치 시 키보드 내려가도록 구현 (#214) * feat: 키보드외 화면 클릭 시 키보드 내려가도록 구현 * refactor: api변경에 다른 dto수정 * feat: 이미지 업로드 및 권한 설정 (#216) * chore: 이미지 권한 추가 * feat: permission manager을 생성하여 권한 체크 및 request * feat: 이미지 추가 버튼을 클릭할 시 권한 설정 연결 * feat: 이미지 피커를 사용하여 uri 전달 구현 * feat: 이미지 파일 업로드 api service 구현 * feat: 이미지 파일 업로드 data source 구현 * feat: 이미지 파일 업로드 repository 구현 * feat: 이미지 파일 martipart로 변환해주는 기능 구현 * feat: 이미지 업로드 관련 뷰 수정 * feat: 이미지 파일 업로드 및 api 연결 구현 * style: ktlint format * fix: git conflict 해결 * refactor: 이미지 scaleType 변경 * refactor: string value 컨벤션 적용 * feat: 토큰 반환 시 body가 아닌 cookie로 반환하도록 원상복구 (#223) * feat: 토큰 재발급 API에서 requestHeader로 refreshToken 받도록 수정 (#227) * feat: 토큰 재발급 API에서 body가 아닌 cookie로 토큰 반환 * feat: 회원가입 API도 body가 아닌 cookie로 토큰 반환 * refactor: service 용 dto 명 컨벤션에 맞춰 수정 * feat: 댓글방 일정 수정 API 구현 (#226) * feat: 댓글방 일정 수정 API 구현 * test: 총대가 아닌 참여자가 공모 일정 정보를 수정할 경우 예외 발생 * feat: 댓글방 상태 조회 시 버튼 텍스트 추가 (#229) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 (#222) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 * refactor: 구현 방식 변경 * style: lint적용 * Feature/217 offering status (#230) * feat: 댓글방 상태 조회 api service 구현 * feat: 댓글방 상태 조회 model 및 dto 구현 * feat: 댓글방 상태 조회 datasource 구현 * feat: 댓글방 상태 조회 repository 구현 * feat: 댓글방 상태 조회 api 연결 구현 * style: ktlint 적용 * feat: 댓글방 상태 변경 (#231) * feat: 댓글방 상태 변경 api service 구현 * feat: 댓글방 상태 변경 data source 구현 * Revert "feat: 댓글방 상태 변경 data source 구현" This reverts commit 052691a8de945c60a60586ee66a05a6a3b264217. * feat: 댓글방 상태 변경 data source 구현 * feat: 댓글방 상태 변경 repository 구현 * feat: 댓글방 상태 변경 api 연결 구현 * style: ktlint 적용 * feature: 카카오 로그인 구현 (#235) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * feat: 공모 참여자 목록 조회 API 구현 (#225) * feat: 공모 참여자 목록 조회 API 구현 * test: 실패 테스트 오류 수정 * style: 띄어쓰기 적용 * refactor: MemberEntity를 받도록 변경 * refactor: isParticipant를 구현하여 가독성 개선 * refactor: 총대를 찾을 수 없는 상황의 예외 추가 * refactor: 참여 검증로직을 서비스로 이동 * refactor: 사용하지 않는 메서드 제거 * refactor: 검증 로직 가장 상단에 위치 * refactor: 총대 추출 로직 수정 --------- Co-authored-by: masonkimseoul Co-authored-by: SCY * refactor: 마감임박순 필터링 쿼리 조건 수정 (#239) * refactor: 마감임박순 필터링 조건 수정 * refactor: 더미 데이터 시간 수정 * fix: 필터링 오류 수정 (#243) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 (#247) * feat: 공동구매 상태 변경 다이얼로그 구현 (#245) * feat: 공동구매 상태 변경 다이얼로그 view 구현 * feat: 공동구매 상태 변경 다이얼로그 Listener 구현 * feat: 공동구매 상태 변경 다이얼로그 연결 및 상태 변경 로직 수정 * test: 테스트 코드 작성을 위한 기본 세팅 (#255) * feat: CoroutinesTestExtension 구현 * feat: Livedata getOrAwaitValue 구현 * feat: InstantTaskExecutorExtension 구현 * feat: TestFixture 생성 * style: ktlint 적용 * feat: 공모글 목록 화면 UI 개선, 공모글 작성에서 낱개 금액이 엔빵 가격보다 저렴할 시 글 작성 막는 기능 구현 (#246) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 구분선을 각각의 아이템의 하단에 넣고 프래그먼트 뷰의 "채팅" 텍스트 밑에 하나 추가 * fix: 텍스트뷰에 font 적용, 마지막 댓글 시간 텍스트를 조금 왼쪽으로 이동 * fix: 낱개 가격 이름을 eachPrice -> originPrice 수정 * fix: 낱개 가격이 엔빵 가격보다 싸면 토스트를 띄우고 글작성을 막는 기능 구현 * fix: 네이티브앱키 로컬프로퍼티로 이동 * refactor: 함수명 변경 * fix: 카카오 계정으로 로그인 후 액티비티 전환하지 않는 문제 수정 * refactor: 사용되지 않는 클래스 삭제 * refactor: 패키지 수정 * refactor: alsong 로그 수정 * refactor: 변수명 수정 * refactor: Manifest의 네이티브앱 키 숨김 * refactor: 로컬프로퍼티의 데이터 형식 수정 * Update android.yml * refactor: alsong 로그 삭제 * ci 빌드 실패가 manifest때문인지 테스트 * refactor: 매니페스트에 앱 키 넣을 수 있게 하는 gradle 설정 수정 * 매니페스트 수정하고 재테스트 * 매니페스트 수정하고 재테스트 * chore: 그래들 수정 * chore: 그래들 수정2 * chore: 그래들 수정3 * chore: 그래들 수정4 * chore: 카카오 계정으로 로그인하는 기능 제외 * feat: 홈화면 테스트 작성 (#257) * chore: mockk의존성 추가 * test: OfferingViewModel 테스트 작성 * style: lint적용 * refactor: stub를 TestFixture로 이동 * test: 댓글방 테스트 코드 작성 (#258) * refactor: 댓글 보내는 함수명 변경 * refactor: 공구 약속 장소 및 시간 캐시 기능 * test: 테스트를 위한 fake repository 구현 * test: 댓글방 viewmodel test 작성 * feat: 댓글방 ActivityTest 작성 * feat: 댓글방 ActivityTest 작성 * style: ktlint 적용 * refactor: test fixture에서 사용하지 않는 것 삭제 * style: ktlint 적용 * feat: GA 모니터링 환경 구축 및 로깅 전략 적용 (#242) * chore: Firebase Crashlytics 의존성 추가 * feat: Firebase 초기화 * feat: FirebaseManager 구현 * feat: 총대가 공구 진행 상황을 다음 단계로 변경했을 때 event 추가 * feat: 로깅 기능 구현 - 검색 - 필터링 - 공모글 클릭 - 공모 참여 * style: lint적용 * feat: 글 작성 완료 시 event 추가 * feat: 로그인 시 event 추가 --------- Co-authored-by: Namyunsuk Co-authored-by: songpink * test: 공모글 작성 이미지 테스트 코드 작성 (#260) * refactor: 상수 가시성 변경 * feat: test fixture 구현 * feat: fake repository 이미지 업로드 기능 추가 * test: OfferingWriteViewModelTest 이미지 업로드 test 코드 작성 * feat: 로그인 후 홈화면으로 이동해도 로그인 화면이 종료되지 않는 문제 수정 (#261) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 로그인 후 LoginActivity가 종료되도록 수정 * feat: 공모 상세 화면 테스트 작성 (#264) * feat: OfferingDetailViewModel 테스트 작성 * refactor: 테스트 수정 * style: lint적용 * style: lint적용 * feat: 로깅 코드 삽입 (#266) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 * feature: 로깅 샘플 구현 * refactor: 불필요한 코드 제거 * feat: logging 적용 --------- Co-authored-by: fromitive * fix: 마감 임박 필터링 쿼리 수정 (#267) * chore: logback 설정 진행 (#270) * chore: logback 설정 * fix: multipart 요청 필터링 * chore: logback 설정 변경 * chore: pull request ci/cd 닫기 * fix: 이미지 업로드 API의 responseBody가 두 번 뜨는 오류 해결 (#273) * fix: 이미지 업로드 API 두 번 도는 문제 해결 * test: 이미지 업로드 API의 누락된 response field 추가 * refactor: 홈화면 수정 (#271) * refactor: 할인율 마진 추가 * refactor: 공구상태에 대한 문구 수정 * refactor: 클릭 시 최상단으로 이동하는 버튼 구현 * feat: 공모글 작성 화면 테스트코드 작성 (#274) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: 공모글 작성 테스트 구현 * feat: 댓글방 목록 화면 테스트코드 작성 (#276) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: "댓글방 목록을 확인할 수 있어야 한다" 테스트 작성 * feat: pageSize validation 추가 (#279) * feat: pageSize validation 추가 * feat: magic number 추출 * fix: 공모 상세 화면 오류 수정 (#280) * fix: 총대 여부 확인 로직 수정 * fix: 마감 임박 시 보여주는 버튼 수정 * fix: 공모 작성 후 홈화면으로 돌아왔을 떄 목록이 새로고침 되지 않는 오류 수정 * test: 테스트 코드 수정 * style: lint적용 * feat: 댓글방 목록 화면 자동 업데이트 되지 않는 문제 수정, 회원가입 이후 자동으로 로그인되지 않는 문제 수정 (#282) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 라이플사이클 오너 설정 * fix: 회원가입 후 자동으로 로그인 되도록 수정 * chore: change version name (#291) * feat: 카카오 계정 로그인 기능 구현 시 CI가 실패하는 문제 해결 (#296) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * feat: 로그인 화면 리팩토링 (#298) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * refactor: SimpleCookieJar의 패키지 변경(presentation 레이어에서 data레이어의 source 패키지로 이동) * refactor: data store를 관리하는 클래스를 생성하고 이 클래스를 사용하도록 변경 * refactor: 사용하지 않는 의존성과 주석 제거 * refactor: http status code 추가 * refactor: 함수분리 * refactor: ktFormat 적용 * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentRooms) * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentDetail), 사용되지 않게 된 memberId 제거 * refactor: ktFormat 적용 * test: 테스트코드 수정 * refactor: Preferences -> DataStore 이름 변경 * refactor: 채팅방 UI UX 개선 (#303) * feat: 키보드가 아닌 다른 영역을 클릭하면 키보드 내리는 기능 구현 * feat: 뒤로가는 버튼 기능 추가 * feat: 댓글 입력 maxLines 설정 및 maxLength 설정 * style: ktlint 적용 * 필요 없는 코드 제거 * feat: 댓글방 목록에서 자신이 총대인 댓글방의 UI 개선 (#304) * refactor: 댓글방의 자신이 총대인 댓글방 ui 개선 * fix: Binding 클래스 네이밍 수정 * feat: 가로모드, 다크모드 설정 (#305) * refactor: api변경에 따른 리팩토링 (#310) * feat: 로그인 화면 해상도 대응 (#313) * feat: 이미지 업로드 중일 때 로딩 상태 설정 (#317) * feat: 공모 글 작성 ui state 구현 * feat: 로딩 progressbar 생성 * feat: UI 상태에 따른 토스트 메시지 처리 * refactor: 잘못된 입력에 대한 에러 처리 변경 * refactor: 홈화면 리팩토링 (#324) * refactor: textSize dp로 변경 * refactor: 검색 버튼 크기 변경 - 검색 버튼 패딩 추가 - 검색창 끝에 패딩 추가 * refactor: 엔터키를 통해 검색하도록 수정 * refactor: 필터 단일 선택되도록 수정 * style: lint적용 * feat: 댓글방 새로운 기능 GA 연결 (#328) * feat: 댓글방 참여자 확인 Event 구현 * feat: 댓글방 상태 변경 다이얼로그 취소 Event * feat: 참여자가 공구에서 참여 포기 Event 구현 * style: ktlint 적용 * test: 테스트 데이터 수정 (#330) * feat: Fragment GA 모니터링 수집 (#332) * feat: fragment logScreenView 추적 함수 구현 * feat: 각 fragment에서 화면 감지 GA 설정 * feat: 마이페이지 기본 세팅 및 뷰 변경 (#335) * feat: 공모 참여 취소 기능 구현 (#318) * test: 공모 참여 취소 테스트코드 작성 * feat: 공모 참여 취소 기능 구현 * refactor: 불필요한 쿼리 메서드 제거 * style: 불필요한 개행 제거 * refactor: 모집중인 상태가 아닌 경우 공모 참여를 취소할 수 없도록 변경 * refactor: 공모 참여 취소 응답 상태 코드 변경 * refactor: 에러 메시지 명확한 문구로 변경 * refactor: query parameter를 적용해 어떤 공모의 참여를 취소할 것인지 의도를 명확하게 전달하도록 변경 * refactor: 총대 검증 메서드 네이밍 명확하게 변경 * feat: 댓글방 생성 시점 변경 (#319) * feat: 댓글방 생성 시점 변경 * refactor: 불필요한 도메인 OfferingWithRole 제거 * refactor: 불필요한 도메인 CommentWithRole 제거 * refactor: 댓글의 작성자 확인 메서드 추가 * refactor: 댓글방 목록 조회 dto 생성자 추가 * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 (#322) * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 * refactor: 로그인용 dto 분리 및 공통 dto에 prefix로 auth 추가 * feat: valid 어노테이션 추가 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 (#323) * refactor: 메서드명 구체적으로 변경 * refactor: 변수명 구체적으로 변경 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 * docs: todo 추가 * refactor: 함수명 통일 * feat: 공모자 여부 필드명 변경 * feat: 댓글방 상태 조회 API 확장 (#325) * feat: 댓글방 상태 조회 API 확장 * refactor: 댓글방 관련 로직 댓글 도메인으로 이동 * feat: LoggingFilter에서 던지는 유효하지 않은 요청에 대한 예외 처리 * refactor: 댓글 관련 엔드포인트 수정 * feat: 댓글방 정보 조회 시 조회 권한을 가진 사용자인지 검증 * refactor: 댓글방 상태 확인 로직 도메인으로 이동 * feat: 상태 변경을 시도하는 사용자가 총대인지 검증 * refactor: 댓글 목록 조회 엔드포인트 수정 * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 (#327) * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 * refactor: Response depth 줄이기 및 DTO 생성자 작성 * fix: imminent 필터 버그 해결 (#337) * fix: 커스텀 필터로 인해 h2-console 접속 깨지는 이슈 해결 (#339) * feat: 마이페이지 기능 구현 (#341) * feat: 마이페이지 닉네임 기능 구현 * feat: 로그아웃 로직 구현 * feat: url 연결 로직 구현 * feat: 필요없는 기능 삭제 * style: ktlint 적용 * feat: 공모 테이블에 할인율과 상태 필드 추가 (#342) * refactor: Condition과 Status 이름 변경 * refactor: 사용하지 않는 DTO 제거 * feat: OfferingEntity에 칼럼 추가 * feat: 공모 거래 날짜 필드 이름 변경 (#348) * fix: 상세화면에서 홈화면으로 갔을 때 상태 변경 안되는 오류 수정 (#343) * refactor: 공모상세페이지 Activity -> Fragment로 리팩토링 * fix: 페이지네이션 및 상태변경 미적용 오류 해결 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 주석 제거 및 상수화 * refactor: livedata 자료형 변경 * refactor: progressbar위치 수정 * refactor: lifecycleScope사용 리팩토링 * refactor: adapter에서 전체 아이템이 아닌 특정 아이템만 notify하도록 리팩토링 * refactor: API변경에 따른 대응 (#352) * refactor: api대응 * refactor: api변경에 따른 테스트 수정 * feat: 공모글 작성 화면 ux 개선 (#344) * fix: 각 항목의 설명을 place holder로 이동 * fix: 필수와 선택 항목의 프래그먼트 분리 * feat: 버튼이 항상 보이도록 수정 * fix: 가격과 총원은 숫자만 입력받도록 변경 * fix: 패딩 수정 * fix: ui 수정 * fix: 도메인 변경에 따른 deadline -> tradeDate 수정 * feat: 필수 항목을 모두 입력하면 선택 항목 화면으로 이동하는 기능 구현 * refactor: ktFormat 적용 * refactor: shared viewModel 사용, 미필수 항목을 미필수 입력 화면으로 이동 * refactor: 프래그먼트 이름 변경 * feat: 입력 숫자의 글자수와 라인수 제한 기능 구현 * fix: 총원이 -1이하로 떨어지는 버그 수정, 공동구매 텍스트 띄어쓰기 제거 * fix: 할인율, 엔빵 금액이 유효하지 않을 때는 "-"로 뜨도록 변경 * fix: 공모를 게시하면 필수, 선택 화면 모두 종료되도록 수정 * fix: 날짜 시간 픽커를 날짜만 선택하는 픽커로 변경 * refactor: ktFormat 적용 * refactor: 바인딩어댑터의 파라미터를 nullable하게 수정 * test: 테스트코드 수정 * feat: 낱개 가격의 place holder로 현재 엔빵 금액을 보여주는 기능 구현 * feat: 내용의 최대 글자수와 현재 글자수를 보여주는 기능 구현 * refactor: ktFormat 적용 * refactor: 공모글 작성시 memberId를 보내지 않도록 변경 * fix: 총원 최대 4자리에서 3자리까지만 입력받을 수 있도록 변경 * fix: deadline -> meetingDate 네이밍 수정 * fix: 공모글 작성 후 작성 화면의 입력값이 초기화되지 않는 버그 수정 * refactor: 네이밍 수정(eachPrice -> originPrice) * refactor: 네이밍 수정(individualPrice -> originPrice) * fix: 내용의 현재 글자수 색이 메인컬러가 되지 않는 문제 수정 * refactor: 프래그먼트 종료될 때 바인딩 해제하도록 수정 * refactor: id가 없는 뷰의 id 추가 * refactor: 함수 분리 * fix: 내용 옆의 * 제거 * fix: GA 이벤트 이름 변경(공모글 작성 - 필수 화면에서의 이벤트임을 명시함) * refactor: og 태그 추출 기능 수정 (#349) * refactor: crawler 패키지 이동 * feat: naver api 클라이언트 추가 refactor: 사용하지 않은 기존 og image 크롤러 명칭 변경 * feat: html 크롤링 방식과 naver api 방식을 조합하는 Extractor 구현 * fix: OfferingService ProductImageExtractor 추상화 * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 (#358) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 * test: 테스트코드 수정 * refactor: 공모글 목록 조회 필터링 수정 및 추가 (#356) * refactor: 마감임박순 필터링 이름 마감임박만으로 변경 Co-authored-by: fromitive * refactor: 필터링 쿼리 수정 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 구현 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 연결 Co-authored-by: fromitive * fix: 쿼리 내 불필요한 파라미터 제거 Co-authored-by: fromitive * refactor: 할인율이 null일 경우 높은할인율 필터링 대상에서 제외 Co-authored-by: fromitive * feat: 참여가능만 필터링 전략 클래스 추가 * feat: 공모 목록 조회 API 응답값 변경 * fix: 높은 할인율 단위 변경 및 last-id 필터링 로직 수정 * style: 주석 제거 --------- Co-authored-by: fromitive * refactor: 할인율 계산 로직 수정 (#359) * refactor: 할인율 계산 로직 수정 Co-authored-by: fromitive * refactor: 소수점 둘째 자리에서 반올림하도록 변경 Co-authored-by: fromitive * test: 할인율 계산 로직 * fix: 할인율 단위 백분율로 수정 --------- Co-authored-by: fromitive * feat: 총 모집 인원 수 최댓값 설정 (#361) Co-authored-by: fromitive * fix: 필터 오류 수정 (#362) * fix: 필터 오류 수정 - '참여가능만'필터 분기처리 제거 * chore: 주석 제거 * feat: API 스펙 변경에 따른 대응 (#364) * feat: 댓글 목록 조회 api 스펙 변경에 따른 대응 * feat: 댓글방 정보 조회 api 스펙 변경에 따른 대응 * feat: 공모 일정 조회 api 스펙 변경에 따른 대응 * feat: 댓글 상태 변경 api 스펙 변경에 따른 대응 * test: api 스펙 변경에 따른 test 코드 변경 * style: ktlint 적용 * feat: remote dto package 분리 * feat: 자동 확정 기능을 위해 스케줄러 적용 (#363) * chore: todo 추가 및 메서드명 변경 * feat: Scheduled 어노테이션 추가 및 Scheduler 분리 * test: ServiceTest 환경 구축 * feat: offeringStatus 변경 로직 추가 * refactor: 수동 확정 로직 추가 및 코드 스타일 수정 * refactor: 자동 확정 로직을 조회에서 Scheduled로 이동 * fix: 마감임박 설정 기준 내일로 변경 --------- Co-authored-by: Choo Co-authored-by: SCY * fix: 공모 작성 후 홈화면 돌아올 때 새로 작성한 글이 보이지 않는 오류 수정 (#369) * feat: Access Token, Refresh Token을 data store에 저장하는 기능 구현 (#372) * feat: 앱 재시작 시 토큰을 데이터스토어에서 꺼내 사용하는 기능 구현 * feat: 로그인이 이미 되어있다면 로그인 화면을 건너뛰는 기능 구현 * feat: 로그아웃 기능 구현 * fix: 마이페이지 화면으로 넘어가면 바텀네비게이션이 사라지는 버그 수정 * fix: 데이터스토어에서 토큰이 꺼내지지 않는 버그 수정 data store에서 토큰을 꺼내는 코루틴 비동기 작업이 끝나기 전에 함수를 종료해 버려서 생기는 버그였습니다. * refactor: ktFormat 적용 * refactor: startActivity 함수를 LoginActivity가 동반객체로 갖고 있도록 변경 * refactor: 함수명과 event명 변경 추가로 GA위치가 조금 잘못된 점이 있어서 수정했습니다. * feat: 공모 상세 화면 추가 기능 반영 (#375) * feat: 신고하기 기능 구현 * feat: 물품 링크가 없으면 보여지지 않도록 구현 * refactor: 마감 시간에서 거래 날짜로 리팩토링 * feat: 이미 참여한 공모게시글에서 채팅방으로 이동하는 기능 구현 * fix: 댓글방 목록의 마지막 댓글방이 보이지 않는 문제 수정 (#376) * fix: 리사이클러뷰 레이아웃의 크기가 화면 밖에 벗어나지 않도록 수정 * fix: 리사이클러뷰 레이아웃의 맨 밑에 구분선 하나 추가 아래로 땡겼을 때 구분선이 사라져버리는게 보기 안좋아서 추가했습니다 * refactor: 코트 포맷 적용 (컨트롤 알트 L) * feat: isManualConfirmed 제거 및 도메인 로직 확인 (#377) * refactor: isManualConfirmed 칼럼 삭제 및 관련 로직 분리 * refactor: 더미 데이터 수정 --------- Co-authored-by: fromitive * feat: API 별 권한 확인 로직 추가 (#371) * feat: 권한 확인 로직 추가 * feat: 인증 필터 적용 * refactor: 더미 데이터 칼럼 위치 변경 (#382) * refactor: 홈화면 api필드 추가에 따른 대응 (#381) * refactor: dto필드 추가 * fix: 상태 변경 오류 해결 * fix: 필터 선택 또는 검색상태일 때 공모 작성 후 나오면 목록 안보이는 오류 수정 * refactor: 세부 주소 api에서 받아오도록 변경 * style: lint적용 * fix: API 문서에 접근할 수 없는 현상 해결 (#384) * fix: API 문서에 접근할 수 없는 현상 해결 * style: 신뢰할 수 있는 URL 개행 수정 * feat: 공모 목록에서 동을 보여주는 기능 구현 (#386) * feat: 공모 단건 조회 API 구현 (#388) * feat: 공모 상세 조회 API 엔드포인트 변경 * feat: 공모 단건 조회 API * style: 공모 관련 API 순서 변경 * test: 불필요한 공모글 생성 코드 제거 * test: 공모 단건 조회 서비스 테스트 * refactor: 상태변경 리팩토링 (#389) * refactor: 공모 상세 조회 api변경 대응 * refactor: 공모 상태 변경 리팩토링 * refactor: 리팩토링에 따른 테스트 수정 * chore: 불필요한 로그 제거 * fix: 댓글 입력 후 뒤로가기 시 최근 댓글이 반영되도록 수정 (#397) * chore: JAR 파일에 OAS 파일 누락되는 이슈 해결 및 중복 task 제거 (#391) * chore: 중복되는 task 제거 * chore: cicd 범위 조정 * fix: 참여자 목록 조회 API에서 totalCount 반환하지 않는 이슈 해결 (#400) * feat: 댓글방 참여자 확인 API 연결 (#401) * feat: 참가자 정보를 가져오는 api service 구현 * refactor: 필요없는 코드 삭제 * feat: 참여 관리 datasource 구현 * feat: 참여자 domain 모델 구현 * feat: 참여를 관리하는 repository 구현 * feat: 참여자 목록을 보여주는 recycler view 연결 및 구현 * refactor: 더보기 버튼 수정 * feat: 필요없는 리소스 파일 삭제 및 상태 기본 이미지 변경 * refactor: 약속 장소 및 시간 ui model 을 사용하여 관리 * refactor: 댓글방의 정보를 불러오는 로직 ui model을 사용하여 관리 * refactor: ui model 변환 로직 변경 * feat: 공동구매 참여 인원 확인 기능 구현 * feat: 신고하기 폼 연결 구현 * test: 코드 변경에 따른 테스트 코드 수정 * style: ktlint 적용 * refactor: xml id 추가 * feat: 댓글방 공동구매 나가기 API 연결 (#402) * feat: 공동구매 나가기 기능 api service 구현 * feat: 공동구매 나가기 기능 data source 구현 * feat: 공동구매 나가기 기능 repository 구현 * feat: 공동구매 나가기 기능 연결 * style:ktlint 적용 * fix: /auth/refresh endpoint accessToken 검증 예외 추가 (#407) * refactor: 더미 데이터 정합성 확보 (#406) * refactor: 더미 데이터 정합성 확보 * refactor: 추가된 칼럼 반영 * feat: CallApiHandler 구현 (#403) * feat: CallApiHandler 구현 * refactor: CommentRoomsDataSource 수정 * feat: CommentRemoteDataSourceImpl 에러핸들링을 통해 수정 * feat: 에러 핸들링에 따른 DataSource 리팩토링 - OfferingDetailDataSource - OfferingRemoteDataSource * feat: ParticipantRemoteDataSourceImpl 에러핸들링을 통해 수정 * style: ktlint 적용 * refactor: AuthRemoteDataSource 수정 * feat: Result의 map 과 getOrThrow 함수 생성 * feat: 에러 핸들링에 따른 Repository 리팩토링 - OfferingDetailRepository - OfferingRepository * refactor: Result 변경에 따른 레포지토리 수정 (AuthRepository, CommentRoomsRepository) * feat: 에러 핸들링에 따른 CommentDetailRepository 리팩토링 * feat: 에러 핸들링에 따른 ParticipantRepository 리팩토링 * feat: 에러 핸들링에 따른 viewmodel 리팩토링 - OfferingViewModel - OfferingDetailViewModel * refactor: 에러 핸들링에 따른 LoginViewModel 리팩토링 * refactor: 에러 핸들링에 따른 CommentRoomsViewModel 리팩토링 * refactor: 토큰 리프레쉬 후 다시 함수 호출하도록 추가 * feat: 에러 핸들링에 따른 CommentDetailViewModel 리팩토링 * refactor: 에러 핸들링에 따른 OfferingWriteViewModel 리팩토링 * refactor: 공모 목록 토큰 리프래시 적용 * fix: 잘못된 코드 수정 * refactor: 필요없는 주석 제거 * refactor: 공모 목록 리팩토링 * fix: 리빌드시 쿠키가 제대로 저장되지 않는 현상 수정 * refactor: 필요없는 코드 삭제 및 상수화 추가 * test: 에러핸들링에 따른 FakeAuthRepository, OfferingWriteViewModelTest 수정 * refactor: ktFormat 적용 * test: 코드 변경에 따른 Fake Repository 변경 * test: CommentDetailViewModelTest 코드 수정 * style: ktlint 적용 * refactor: 가독성 개선(에러 로그 함수명 추가, Success가 Error보다 위에 나오도록 수정) * refactor: 불필요한 로그 제거 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 람다 넘겨주는 방식 수정 * style: lint 적용 * test: 테스트코드 수정 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: Namyunsuk * feat: proguard를 사용한 난독화 적용 (#413) * chore: 환경에 따른 yml 파일 분리 (#411) * chore: 환경 별로 yml 파일 분리 * chore: 불필요한 yml 설정 제거 * fix: 공구 상세 페이지 오류 해결 (#417) * fix: 바로가기 클릭되지 않는 오류 수정 * refactor: 주소 표시할 때 최대 2줄까지 그리고 넘어갈 시 말줄임 나오도록 수정 * refactor: 공모 목록, 공모 상세 에러 핸들링 (#418) * refactor: 공모 목록에서 401에러를 제외하고는 에러코드 올 시 빈화면 보여주도록 에러핸들링 수정 * refactor: 필터및 업데이트된 공모 목록 가져오는 로직 에러핸들링 수정 - 400: 토스트 메시지 띄어줌 - 401: refresh - 그외에는 로그로 에러 코드를 보여줌 * refactor: strings네이밍 통일 * refactor: 공모 상세 에러 핸들링 수정 * refactor: strings정리 - offering_detail부분 정리 * feat: 카카오 로그인 중 사용자 정보 확인 로직을 안드로이드에서 백엔드로 이관 (#404) * feat: 카카오 로그인 API 구현 * feat: providerId를 loginId로 수정 * feat: 소셜 로그인 시 랜덤 생성된 비밀번호 사용 * refactor: 불필요한 api 제거 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY * test: 로그인 로직 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY * test: MemberFixture 불필요한 함수 제거 및 통일 Co-authored-by: fromitive Co-authored-by: Dora Choo * refactor: 불필요한 정보 제거 Co-authored-by: fromitive Co-authored-by: Dora Choo * feat: 카카오 로그인 에러 핸들러 추가 Co-authored-by: fromitive Co-authored-by: Dora Choo * feat: 민감 정보 로깅에서 제외 Co-authored-by: fromitive Co-authored-by: Dora Choo --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY Co-authored-by: fromitive * feat: cookie 관련 예외 처리 (#409) * refactor: 더미 데이터 http 추가 (#422) * fix: 더미데이터 정합성 맞추기 (#425) * feat: 로그인 api 변경 반영 (#426) * feat: 카카오 로그인 후 총대마켓 서버로 email을 보내던 방식에서 카카오 access token을 보내는 방식으로 변경 * feat: login과 signup을 하나로 api로 통합된 것 반영 * refactor: ktFormat 적용 * refactor: 테스트코드 수정 * feat: 로깅 시 UUID가 아닌 회원 번호가 기록되도록 변경 (#428) * feat: logging 시 memberId가 나오도록 기능 추가 * feat: logging 시 memberId 및 identifier가 함께 나오도록 변경 * refactor: lombok getter 적용 * feat: Spring Timezone KST로 설정 (#430) * chore: Dockerfile 타임존 변경 (#432) * fix: Offering 목록 조회 시 NPE 해결 (#434) * refactor: 에러 핸들링 리팩토링 (#436) * feat: 리프레시 토큰 만료 시 데이터스토어를 비우고 로그인 화면으로 이동하는 기능 구현 (#438) * feat: 댓글방 에러 헨들링 (#439) * refactor: refresh시 401이 오는 경우에 대한 에러핸들링 추가 (#441) * chore: 버전 업데이트 (#443) * refactor: 외래키 필드 notnull 조건 추가 (#445) * chore: prod CI/CD 구축 (#423) * chore: 환경 별로 yml 파일 분리 * chore: 운영 서버 CI/CD 스크립트 작성 * chore: 운영 환경 내 swagger 문서 제거 * chore: 운영 환경 포트포워딩 명령어 제거 * chore: prod ci/cd 스크립트 트리거 추가 * chore: prod ci/cd 스크립트 트리거 변경 * chore: prod ci/cd 스크립트 트리거 path 구체화 * chore: prod ci/cd 스크립트 docker 실행 명령어 오타 수정 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev 및 prod ci/cd 스크립트 data.sql 실행 비활성화 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev script test --------- Co-authored-by: Choo * chore: prod 불필요한 트리거 주석 처리 (#447) * merge: v1.1.0 to develop-BE * feat: 게시글 상세 화면 구현 (#8) * feat: 게시글 상세 화면 레이아웃 작성 * feat: Data layer코드 작성 * refactor: dto패키지 분리, dto에 serialName추가 * refactor: 도메인 모델 수정 - 가변에서 불변으로 변경 - 사용하지 않는 메서드 제거 * refactor: 공통으로 사용되거나 사용될 수 있는 확장함수를 별도의 파일로 분리 * style: lint 적용 * refactor: 메서드명 컨벤션 적용 * refactor: request Dto에 SerialName적용 * refactor: 메서드명 수정 * feat: 도메인 추가 (#15) * feat: BaseTimeEntity 추가 * feat: Member Entity 추가 * feat: Offering Entity 추가 * feat: OfferingMember Entity 추가 * feat: Comment Entity 추가 --------- * feat: BottomNavigation 구현 (#16) * chore: jetpack navigation 라이브러리 추가 * feat: 필요한 바텀 네비게이션 리소스 추가 * feat: bottom navigation fragment 추가 * feat: bottom navigation graph 구현 * refactor: 컨벤션에 맞게 id 수정 * feat: 공동구매 상세 조회 기능 구현 (#18) * chore: h2 환경설정 추가 * docs: http client 추가 * refactor: entity 접미어 적용 * chore: dummy data 추가 * docs: http client 값 변경 * refactor: repository 와 domain 패키지 분리 * feat: 공동구매 상세 조회 API 구현 * refactor: entity 접미어 적용 * style: 클래스 컨벤션 적용 * chore: h2 console 설정 제거 * refactor: OfferingCondition enum값 결정로직을 enum 안으로 이동 * feat: 홈화면, 마이페이지 화면 레이아웃 작성 (#19) * refactor: FragmentContainer width 속성 수정 * feat: 홈 화면 레이아웃 작성 * feat: 마이페이지 화면 레이아웃 작성 * fix: 플로팅 버튼이 홈에서만 보이도록 수정 * refactor: 리소스 네이밍 컨벤션에 맞게 수정 * feat: API 문서화 적용 (#23) * chore: springdoc-openapi 의존성 추가 * chore: springdoc 설정 추가 * feat: SwaggerConfig 파일 추가 * feat: 공모 상세 조회 API 문서화 --------- * fix: 공모 상세 조희 API의 price 필드 자료형 변경 및 memberId 필드 추가 (#28) * fix: 상세조회 API 금액 필드 자료형 변경 * fix: memberId 추가 * 내가 쓴 글인지 아닌지 확인 위해 --------- * chore: 백엔드 CI 및 도커 파일 작성 (#27) * chore: actions 적용 브랜치 설정 (#30) * chore: actions 적용 브랜치 설정 * chore: path 및 ref 태그 제거 * chore: working-directory 태그 추가 * chore: Dockerfile jar 경로 수정 * feat: 댓글방 목록 구현 (#26) * feat: 댓글방 목록 UI 구현 * fix: 구분선을 ImageView에서 View로 변경 * feat: 댓글방 목록 도메인 모델 구현 * feat: 댓글방 어답터 구현 * feat: "채팅" string 추가 * refactor: 불필요한 코드 제거 * fix: xmls 중복 속성 제거 * refactor: 댓글방 클래스들을 comment 패키지로 분리 * refactor: 컬러와 폰트 사이즈를 values 파일로 분리 * feat: 공모 목록 조회 기능 구현 (#35) * feat: 공모 목록 조회 API 구현 * docs: 공모 목록 조회 API http client에 추가 * fix: 공모 상세 조회 API의 status 필드를 condition으로 명칭 변경 * feat: 공모 목록 조회 API의 isClosed 필드 이름을 isOpen으로 변경 * feat: 댓글방 디테일 화면 구현 (#32) * feat: font 설정 * feat: vector 이미지 추가 * feat: 채팅 아이템 뷰 구현 * refactor: 컨벤션에 맞게 네이밍 수정 * feat: 댓글 입력 edit text 구현 * chore: 백엔드 CD 스크립트 작성 (#34) * chore: 백엔드 CD 스크립트 작성 * chore: 도커 백그라운드로 실행 * chore: 도커 설정 및 트리거 설정 변경 * chore: 도커 이미지 제거 로직 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 강제 제거하도록 수정 * chore: gradle 캐싱 로직 추가 (#39) * chore: gradle 캐싱 로직 추가 * chore: 이벤트 트리거 조건 수정 * feat: 공모 참여하기 기능 구현 (#40) * fix: BaseTimeEntity 적용 오류 수정 * feat: 참여하기 API 구현 --------- * feat: 공모 상세 조회 API에 참여자 목록 필드 추가 (#42) * feat: 공모 상세 조회 API의 request에 memberId 필드 추가 (#45) * feat: 공모 참여 API의 불필요한 응답값 전부 제거 (#48) * feat: 공모 참여 API의 불필요한 반환값 제거 * chore: 자주 쓰는 h2 console enabled 설정 주석 처리 * feat: 이미 참여한 공모에 참여 못하게 예외 처리 (#51) * feat: 공모 상세 페이지 API 연결 (#46) * build: 불필요한 의존성 제거, properties관련 코드 작성 * refactor: base_url코드상에서 제거 * feat: api수정에 따른 필드 변경 및 네이밍 반영 * refactor: 네이밍 변경 * refactor: OfferingDetail의 변경, mapper변경 * refactor: service분리 * refactor: DataSource, Repository분리 * refactor: API변경에 따른 리팩토링 * feat: 공모 상세 조회 기능 구현 * refactor: 참여하기 api변경에 따른 data, domain 코드 수정 * feat: 공모 상세 페이지 참여하기 기능 구현 * feat: 공모 상세 화면에서 이미지를 불러올 수 없을 시 기본이미지를 보여주는 기능 구현 * feat: 게시물 상세 화면 폰트 적용 * style: lint적용 * refactor: 액티비티 destroy시 binding해제하도록 코드 추가 * refactor: glide옵션 변경 - 에러 발생 시 보여줄 이미지 - url이 null일 시 보여줄 이미지 * refactor: viewModel에 custom getter추가 * fix: 내용이 짧을 시 뒷 배경이 회색으로 보이는 버그 수정 * fix: 참여하기 버튼을 눌렀을 시 텍스트가 바뀌지 않는 버그 수정 * feat: 테스트 데이터 다양화 (#52) * refactor: 공모 엔티티에 currentCount 필드 추가 (#55) * feat: 댓글 작성 API 구현 (#57) * feat: 댓글방 내 공모 일정 조회 기능 구현 (#58) * feat: 댓글방 내 공모 일정 조회 기능 구현 * refactor: 공모 일정 조회 api 명세 변경 --------- * refactor: common 패키지명을 global로 변경 (#61) * chore: 안드로이드 CI 파일 작성 (#63) * feat: 댓글 목록 조회 API 구현 (#66) * chore: build CI 작업을 위한 manifest 파일 수정 (#65) * chore: 알람 권한 추가 * chore: local properties 속성 추가 * chore: local properties null 체크 로직 추가 * chore: buildConfigField null 체크 * style: lint 적용 * chore: secret 값 설정 * fix: secret 값 오류 수정 * fix: 문법 오류 수정 * chore: 경로 수정 * chore: 문법 수정 * style: lint 적용 * feat: 댓글방 목록 조회 API 구현 (#70) * feat 댓글방 접히는 공지 뷰 구현 (#72) * chore: manifest에 CommentDetailActivity 추가 * feat: BindingAdatper을 사용하여 접힐 때 애니메이션 적용 및 픽셀 변환 * feat: viewmodel 구현 및 click 마다 접히고 펴지는 로직 구현 * style: ktlint 적용 * refactor: binding adpater을 사용하여 가시성 변경 * refactor: 댓글방 및 댓글 목록 조회 서비스 계층 (#78) * fix: 댓글방 목록 조회 시 가장 최근 댓글 조회 (#80) * feat: 홈화면 API 연결 (#74) * refactor: API변경에 따른 data, domain 코드 변경 * feat: 공모 목록 기능 구현 * refactor: 함수 분리 * style: lint적용 * style: font 적용 * fix: 시간순 정렬 쿼리 추가 (#83) * chore: 더미 데이터 추가 (#87) * feat: 댓글방 목록 API 연결 (#82) * feat: bottom navigation fragment 추가 * feat: vector 이미지 추가 * feat: 댓글방이 없으면 "채팅 목록이 없어요" 라는 텍스트뷰와 이미지뷰를 띄우는 기능 구현 * feat: 댓글방 띄우는 기능 구현 * test: 댓글방 UI 테스트 작성 * refactor: 테스트 클래스명 수정 * refactor: 줄바꿈 수정 * feat: 댓글방 API 서비스 구현 * refactor: API 명세에 따라 도메인 모델 수정 * feat: API 연결 * refactor: API명세에 따라 데이터바인딩 변수명 수정 * feat: 댓글방 목록 API 연결 * refactor: ktlint Format 적용 * refactor: 메모리 누수 방지를 위해 fragment가 destroy 될 때 _binding을 null로 설정 * refactor: 어답터를 방어적복사 하지 않아도 되어서 수정 * refactor: 채팅방이 없다는 이미지뷰를 띄워주는 방식 수정(바인딩 어댑터 수정) * refactor: 함수분리 * refactor: ktFormat 적용 --------- * feat: 댓글방 접히는 공지 API 연결 (#85) * feat: 미팅 일정 API 연결을 위한 data layer 구현 * feat: 미팅 일정 API 연결을 위한 domain layer 구현 * feat: 미팅 일정 API 연결을 위한 presentation layer 구현 * style: ktlint 적용 * feat: 공동 구매 제목 databinding 적용 * refactor: 변수명 수정 * fix: 펼치기 접기 버튼 로직 반대로 수정 * style: ktlint 적용 * chore: 더미 데이터 바로가기 url 수정 (#93) * feat: 공모 상세 페이지 기능 추가 (#94) * chore: 마이페이지 닉네임 임시로 지정 * feat: 바로가기 기능 구현 * feat: 참여버튼 클릭 시 댓글방으로 가도록 기능 구현 * feat: 신고하기 이미지 추가 * style: lint적용 * refactor: 불러오는 공모 페이지 사이즈 변경 * refactor: 댓글 도메인 코드 리팩터링 (#96) * refactor: 로그인 멤버 변수명 변경 * refactor: JPQL 쿼리 컨벤션 및 멤버로 공모 조회 메서드명 변경 * refactor: 최근 댓글 응답 클래스명 변경 * refactor: 컨트롤러 및 서비스 API 순서 변경 * refactor: 로그인 사용자 유효성 검증 * feat: 댓글방 댓글 작성 api 연결 (#95) * chore: windowSoftInputMode 추가 * feat: post comment api service 구현 * feat: post comment DataSource 구현 * feat: post comment Repository 구현 * feat: post comment Presentation 구현 * chore: 더미 데이터 시간 변경 (#100) * feat: 댓글방 입장 기능, 본인이 총대인 방은 다르게 보이는 기능 구현 (#99) * feat: 댓글방의 마지막 댓글 시간을 띄우는 기능 구현 * feat: 자신이 총대인 댓글방을 표시하는 기능 구현 * feat: 댓글방 목록을 클릭해 댓글방 상세로 이동하는 기능 구현 * test: UI테스트 수정 * refactor: 클릭시 id 뿐만 아니라 title도 받아오는 방식으로 수정 * refactor: 오전/오후와 시간을 텍스트뷰에 띄우는 바인딩 어댑터를 DateTimeFormatter의 기능을 사용하는 것으로 수정 * refactor: memberId를 local.properties의 token을 가져다 쓰는 것으로 변경(임시 조치) * refactor: 댓글방 목록의 시간을 띄우는 바인딩 어댑터의 속성명을 수정함 * refactor: 데이터바인딩 variable 변수명을 구체적으로 수정, 일관성을 위해 앞에 `on` 붙임 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정(빠트린것 수정함) * feat: 전반적인 예외 처리 (#103) * feat: 예외 처리 핸들러 추가 * feat: Offering 예외 처리 코드 추가 * feat: Comment 예외 처리 코드 추가 * feat: Member 예외 처리 코드 추가 * feat: OfferingMember 예외 처리 코드 추가 * feat: Offering 예외 처리 상세 코드 추가 * feat: 에러 코드 적용 * feat: 도메인 검증 로직 * feat: DTO 검증 로직 --------- * feat: swagger와 restdocs 연동 (#104) * chore: swagger ui 정적 파일 설치 및 static routing 세팅 * chore: restdocs-api-spec을 이용한 OAS 생성 * chore: swagger ui 정적 파일을 swagger-ui 디렉토리로 이동 * chore: swagger ui 정적 파일 및 static routing 세팅 제거 * chore: 생성된 OAS 파일을 Swagger 디렉터리로 복사하는 스크립트 작성 * chore: openapi3 yaml 파일 gitignore 처리 * chore: static routing 세팅 다시 추가 openapi3.yaml을 사용하기 위함 * test: RestAssured RestDocs 테스트 코드 작성 * test: 공모 목록 조회 API 문서화 * test: 공모 일정 조회 API 및 공모 참여 API 문서화 * test: 댓글 관련 API 문서화 * docs: 논의된 TODO 제거 * refactor: swagger 어노테이션 제거 * chore: 개발 API 서버 목록 설정 --------- * refactor: 에러메시지 필드명 변경 (#108) * fix: restdocs 관련 테스트 실패 이슈 해결 (#106) * chore: cicd 테스트 * chore: 테스트 위해 actions 범위 조정 * chore: 배포 스크립트 띄어쓰기 오타 수정 * chore: 빌드 캐싱 제거 * chore: logging * chore: --warning-mode all 옵션 줘서 gradle 호환 무시하도록 설정 * fix: status 달라서 실패하는 테스트 수정 * chore: actions 범위 수정 * chore: action 범위 수정 * chore: test용 static 파일 추가 * chore: static 하위 폴더를 jar 파일에 포함하도록 설정 * chore: swagger-ui 하위 폴더 제거 * chore: task 순서 조정 * chore: build 스크립트 수정 * chore: 불필요한 설정 변경 제거 * chore: clean build 대신 clean bootJar 사용 * chore: clean, build 각각 하도록 변경 * chore: test 까지 두 번 돌리도록 수정 * chore: openapi3까지 두 번 실행하도록 수정 * chore: copyOasToSwagger 까지 두번 실행하도록 수정 * chore: actions 활성화 범위 수정 * fix: 댓글방 목록 조회 시 참여자 수 조건 추가 (#111) * fix: 댓글방 조회 테스트 수정 (#113) * feat: 홈 화면 무한 스크롤 기능 구현 (#109) * build: pagination라이브러리 추가 * feat: 홈 화면 무한 스크롤 기능 구현 * fix: 마지막 댓글 response를 nullable하게 수정 (#115) * fix: 마지막 댓글 response를 nullable하게 수정 * refactor: ktFormat 적용 * feat: 댓글방 댓글 조회 api 연결 (#116) * feat: dto 및 mapper 구현 * feat: 댓글방 목록 service 구현 * feat: 댓글방 목록 data source 구현 * feat: 댓글방 목록 repository 및 model 구현 * feat: 댓글방 목록 view type을 활용한 recyclerview 구현 및 데이터 바인딩 * feat: polling 기능 구현 * feat: 댓글 스크롤 구현 (새로운 댓글이 생길시 스크롤 아래로) * feat: 총대와 다른 참가자 이미지 리소스 파일 * feat: 댓글방 디테일 공동 구매 상태별 관리 (#117) * feat: 공동구매 상태 관리 리소스 파일 * feat: 공동구매 상태를 관리하는 enum class 구현 * feat: 데이터바인딩을 사용하여 공동 구매 상태 뷰 업데이트 구현 * style: ktlint 적용 * feat: 공동구매 상태 관리 리소스 파일 추가 * fix: 이미지 링크 임시 수정 (#119) * fix: 이미지 링크 수정 (#120) * refactor: 네이밍 수정 (#123) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 (#125) * refactor: 뷰모델 팩토리 방식 변경 (#130) * refactor: 뷰모델 팩토리를 뷰모델의 동반객체로 이동 * style: lint적용 * refactor: Service분리 (#132) * refactor: service분리 * refactor: 패키지명 변경 * style: lint적용 * feat: 공모글 작성 UI 구현 (#134) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 뷰 구현 * fix: 뷰 수정사항 반영 * fix: @+id로 참조하는 부분을 수정 * fix: drawable의 네이밍에 where을 추가 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 (#136) * feat: 참여자 목록 drawer에 필요한 리소스 파일 추가 * refactor: 채팅 text gravity 수정 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 * style: ktlint 적용 * refactor: drawer early return 하는 방식으로 변경 * refactor: ivMore -> ivMoreOptions으로 네이밍 변경 * feat: 공구 참여자 item view 및 댓글방 view 사용자 친화적으로 수정 * chore: CI 빌드 스크립트 중 중복되는 task 제거해 성능 개선 (#128) * chore: jar태스크 비활성화하고 bootJar 태스크로만 JAR 파일 생성 * chore: cicd 범위 조정 * feat: 공모 작성 API 구현 (#139) * feat: 공모 작성 API 구현 * refactor: create를 save로 변경 * refactor: dto entity 매핑로직을 dto로 이동 * refactor: controller request 매개변수 명 컨벤션 적용 --------- * refactor: 공모에 저장하는 주소 값 구체화 (#141) * refactor: 공모에 저장하는 주소 값 구체화 * chore: github-action 스크립트 수정 * chore: CI/CD test 설정 추가 * chore: static/swagger-ui 폴더 추가 * chore: 설정 원상 복구 * chore: ci/cd 범위 수정 --------- * feat: 홈화면(공모목록) UI 추가 구현 및 상태 변경 대응 (#142) * feat: 공모의 상태 변경이 반영되도록 기능 구현 * feat: 공모 목록 ui변경 * feat: 필터 ui추가 * feat: API변경에 따른 DTO수정 * style: lint적용 * feat: resource추가 * refactor: ui위치 수정 * chore: 불필요한 괄호 제거 * refactor: item 수직 정렬 * feat: 댓글방 메시지 조회 시 commentId 필드 추가 (#150) * feat: OG 태그 크롤링 API 구현 (#148) * feat: OG 태그 크롤링 API 구현 * refactor: OG 태그 크롤링 API 엔드포인트 수정 --------- * refactor: 제품 코드와 API 문서 동기화 (#153) * refactor: API 문서 개선 (#157) * refactor: 댓글 작성 시 성공 상태 코드 변경 * refactor: 요청 필수 상태 설명 추가 --------- * feat: s3 이미지 업로드 API 구현 (#147) * feat: s3 이미지 업로드 API 구현 * chore: cicd 액션 범위 수정 * fix: 이미지 업로드 경로의 특수문자 제거 * chore: yml multipart 설정 추가 * chore: S3 업로드 결과 테스트 * fix: inputstream 변환로직 위치 이동 * fix: 업로드할 s3 path 올바르게 수정 * fix: 사진 url 속에 버킷이름을 cloudfront 도메인으로 수정 * chore: actions 범위 재조정 * feat: API endpoint 변경 * chore: docker image 지우는 작업을 마지막으로 이동 * chore: 다른 브랜치로 이전 커밋 이동하기 위해 제거 * chore: 충돌 해결 및 코드 스타일 변경 * test: S3 이미지 업로드 성공 케이스 추가 * test: multipart form data 문서화 * test: 공모 상태 enum 문서화 * feat: 파일 업로드 크기 제한 100MB에서 20MB로 변경 --------- * feat: 주소검색 기능구현 (#161) * refactor: 네이밍 컨벤션 적용 * build: webview 라이브러리 추가 * feat: 스크립트 실행위한 html파일 추가 * refactor: 인터페이스명 변경에 따른 변경 * feat: 주소검색 다이얼로그 레이아웃 작성 * feat: 주소검색 기능 구현 * style: lint적용 * refactor: 불필요한 코드 제거 * build: Firebase의존성 추가 (#165) * feat: 공모글 작성 API 연결 (#162) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 API 연결 구현 * feat: 공모글 작성 뷰모델 구현 * fix: edit text 데이터바인딩 추가 * chore: 테스트를 위해 MutableLiveData default값 넣어둠 * chore: deadline defualt값 형식에 맞게 수정 * feat: 글작성 화면을 액티비티에서 프래그먼트로 수정 * chore: 테스트목적이었던 주석과 mutable livedata 디폴트값 제거 * refactor: 임시 함수명 수정 * fix: 글작성 프래그먼트가 올라오기 전에 바텀 네비게이션이 사라지는 문제 수정 * feat: 필수 항목이 모두 입력되어야 버튼이 활성화 되는 기능 구현 * feat: 가격, 총원 입력이 잘못되었을 시 토스트를 띄우는 기능 구현 * fix: 버튼 비활성화 시 텍스트 변경 * feat: 앱 아이콘 변경 * feat: 앱 이름 변경(chongdae -> 총대마켓) * feat: 예상 엔빵 가격을 보여주는 기능 구현 * refactor: 상수화 * refactor: 예상 엔빵 가격에 ,가 들어가는 기능 구현, 콜론 뒤 white space 추가 * feat: 공구 할인율을 계산해 주는 기능 구현 * feat: +, - 버튼으로 총원을 조절하는 기능 구현 * fix: 할인율과 엔빵가격 계산 시 0으로 나눠지는 상황을 제거 * fix: 맞춤법 수정 할인률 -> 할인율 * fix: 총원 버튼 크기가 너무 작아서 확대 * fix: 항목간 간격이 좁아서 확대 * refactor: Offering Write의 API service, DataSource, Repository를 Offerings와 합침 * refactor: 디버깅용 코드 삭제 * refactor: 버튼 활성화/비활성화를 selector와 삼항연산자로 구현 * refactor: 바인딩어댑터 대신 뷰모델이 visibility 상태를 갖고 있는 방식으로 변경 * refactor: 바인딩어댑터 대신 xml에서 처리하는 방식으로 변경 * refactor: 총원 디폴트 라이브데이터값 상수화 * refactor: +, - 텍스트뷰 버튼으로 수정 * refactor: textStyle bold대신 fontFamily suit_bold를 쓰는 것으로 수정 * refactor: 변수명 뒤에 Int를 붙이는 것 대신 Value를 붙이는 것으로 수정 * refactor: 글작성 제출 버튼의 아이디를 추가 * refactor: ktFormat * refactor: 토스트를 띄우는 함수 분리 * refactor: 도메인 객체 분리 * refactor: UI모델 적용 * refactor: ktFormat 적용 * feat: 댓글방 디테일 Room을 사용하여 data 저장 (#166) * feat: local database 구현 * feat: entity 구현 * feat: dao 구현 * feat: LocalDataSourceImpl 구현 * feat: entity mapper 구현 * refactor: CommentResponse 에 id 값 추가 * refactor: datasource 이름 변경 및 패키지 변경 * refactor: article -> offering으로 네이밍 변경 * refactor: repository 패키지 변경에 따른 수정 * refactor: datasource 패키지 변경 및 local 과 remote 분리 * refactor: repository Application 클래스를 통한 주입으로 변경 * style: ktlint 적용 * refactor: api service 리네이밍 * refactor: git conflict 해결 * refactor: 함수 이름 컨벤션에 맞도록 변경 (getMeetings -> fetchMeetings) * chore: CI 스크립트 추가 (#173) * chore: ci 스크립트 추가 * chore: ci 스크립트 수정 * fix: og 태그 추출 시 크롤링 이슈 해결 (#174) * feat: 날짜, 시간 선택 기능 구현, 주소검색 기능 연결 (#171) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 모집마감 시간 클릭 시 date time picker를 띄우는 기능 구현 * feat: 날짜, 시간 선택 기능 구현 * feat: 주소 검색 기능 연결 * refactor: 함수명 수정, 함수분리 * refactor: ktFormat 적용 * refactor: string으로 분리, 상수화 * fix: string 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정3 * chore: CI workflow 파일 수정4 * feat: 공모가 정상적으로 게시되었을 시 "공모가 게시되었어요!" 라는 토스트를 띄우고 공모글 작성 프래그먼트를 종료하는 기능 구현 * feat: 토스트가 화면 중앙에 뜨는 문제 수정 * refactor: 사용되지 않는 파일 삭제 * refactor: xml 뷰 id 수정 * refactor: 버튼이 TextView인 문제 수정 * refactor: 사용되지 않는 data binding variable 제거 * refactor: 함수명 수정 * refactor: 다이얼로그, dateTimePickerBinding 전역으로 선언 * refactor: dateTimePicker 클릭 이벤트를 추상화 해 xml에서 처리하도록 변경 * refactor: ktFormat * feat: 상품 URL 이미지 추출 API 연결 (#180) * refactor: 사용하지 않는 파일 제거 * refactor: 가시성 변경 * feat: api service 구현 * feat: datasource 구현 * refactor: repository 네이밍 수정 (offeringsRepository -> offeringRepository) * feat: 사진 업로드 관련 리소스 파일 추가 * feat: repository 및 model 구현 * feat: 이미지 링크를 통한 크롤링 이미지 불러오는 api 연결 및 이미지 삭제 로직 구현 * style: ktlint 적용 * refactor: 이미지 prefix 추가 및 에러 메시지 수정 * refactor: build 오류 수정 * fix: git conflict 해결 * feat: 공모 목록 조회 API에 필터링과 검색 기능 추가 (#169) * feat: 공모 필터 목록 조회 API 구현 * test: 공모 필터 목록 조회 API 테스트 * style: 개행 형식 통일 * feat: 공모 필터 목록 조회 API Specification 도입 준비 * fix: url에 큰따움표 제거 * feat: Specification 도입 * refactor: queryString 구체화 * refactor: 함수명 변경 * feat: 최신순 필터링 적용 * feat: 마감임박순 필터링 적용 * feat: 높은할인률순 필터링 적용 * refactor: 전략 패턴 적용해 여러 갈래의 분기문과 중복되는 코드 처리 * test: 변경된 API 스펙에 맞게 문서화 작업 * refactor: 관련있는 메서드들끼리 모이게 순서 재배치 * refactor: 맞춤법 수정 * style: 개행 제거 --------- * feat: 상태 변경 API 구현 (#175) * feat: 댓글방 상태 변경 및 조회 API 구현 * feat: 공모글 상태 조회 API 구현 * feat: 댓글방 상태 변경 중 수동 확정 기능 구현 * refactor: 상태 변경 관련 메서드명 수정 * refactor: 추상 클래스 메서드 컨벤션 통일 * refactor: errorCode 사용 시 클래스 명시 * refactor: 댓글방 상태 관련 API 엔드포인트 수정 및 패키지 변경 * refactor: 댓글방 상태 변경 API HTTP 메서드 수정 * feat: 공모 모집 자동 확정 시 댓글방 상태 변경 --------- * feat: 로그인 기능 구현 (#177) * feat: password 일방향 암호화 기능 구현 * feat: cookie 생산-소비 기능 구현 * chore: jwt 관련 의존성 추가 * feat: 토큰 생성 기능 구현 * feat: 로그인 API 구현 * test: 로그인 API 테스트 * feat: 회원가입 API 구현 * test: 회원가입 API 테스트 * feat: 닉네임 생성 기능 구현 * test: 닉네임 생성 기능 테스트 * fix: postconstruct 여러 개라 발생한 에러 해결 * feat: 회원가입 응답값에 랜덤생성한 닉네임 추가 * feat: MemberArgumentResolver 구현 * feat: MemberArgumentResolver 일부 적용 * test: 바뀐 스펙에 맞게 변경 * test: TestConfig 설정해 빈충돌 오류 해결 * test: 공모 작성 API로 MemberArgumentResolver 사용 * feat: 토큰 재발급 API 구현 * test: 토큰 재발급 API 테스트 * test: 토큰 재발급 API 에러 테스트 * feat: MemberArgumentResolver commant에 적용 * feat: MemberArgumentResolver offering에 적용 * feat: MemberArgumentResolver participant에 적용 * refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경 * refactor: 클래스명 일관적으로 변경 * refactor: 직관적인 명명으로 enum 네이밍 변경 * refactor: Custom Exception 적용 * refactor: 컨트롤러 메서드에 접근제어자 명시 * fix: 중복된 enum 값 제거 * test: 바뀐 API 스펙에 맞게 변경 --------- * fix: nicknameWordInitializer 설정 오류 해결 (#182) * fix: keyword null일 때 처리 및 docs에서 required 제거 (#184) * fix: keyword null일 때 처리 * test: optional() 붙여서 required 제거 * chore: 브랜치에 상관없이 pr 머지 시 자동으로 관련 이슈 닫는 스크립트 구현 (#187) * fix: og 이미지 태그 크롤링 문제 해결 (#190) * refactor: 댓글방 상태 도메인 설계 변경 (#189) * feat: 공모 목록 API 응답값에 낱개 가격 추가 (#193) * chore: readtimeout 5초로 수정 (#195) * feat: 댓글방 상태 조회 시 상태별 이미지 함께 반환 (#196) * feat: 공모 목록 조회 API연결 (#201) * refactor: Condition 수정에 따른 변경 * refactor: api변경에 따른 리팩토링 * refactor: api변경에 따른 목록 무한 스크롤 기능 리팩토링 * feat: 검색 기능 구현 * feat: 필터링 기능 구현 - 참여 가능은 서버 에러로 추후 추가 예정 * feat: 아이템을 불러온 후 recyclerview의 최상단으로 이동하는 기능 구현 - 검색, 필터링 수행 후 최상단으로 이동 * feat: 필터링 목록 불러오는 api연결 * feat: 마감임박 상태 추가 * refactor: default parameter제거 * style: lint적용 * feat: 토큰 반환 시 cookie가 아닌 body 사용하도록 변경 (#206) * feat: 발급한 토큰을 header가 아닌 body로 반환하도록 수정 * refactor: 사용안하는 클래스와 메서드 제거 * test: 바뀐 API 스펙에 맞게 명세 수정 * feat: 이미지 더미 데이터 수정 및 부정확한 가격 데이터 수정 (#207) * refactor: 공모 글 작성 시 총대 참여자 추가 (#208) * feat: 바텀 네비게이션 고정 기능 구현 (#211) * feat: 데이터에서 5자 이상 제거 (#212) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 (#202) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 * refactor: 도메인 명칭 변경 (낱개가격 -> 원가격) * refactor: 도메인 명칭 변경 (공모 -> 댓글방) * refactor: originPrice로 http client 변경 * feat: 키보드 이외 영역 터치 시 키보드 내려가도록 구현 (#214) * feat: 키보드외 화면 클릭 시 키보드 내려가도록 구현 * refactor: api변경에 다른 dto수정 * feat: 이미지 업로드 및 권한 설정 (#216) * chore: 이미지 권한 추가 * feat: permission manager을 생성하여 권한 체크 및 request * feat: 이미지 추가 버튼을 클릭할 시 권한 설정 연결 * feat: 이미지 피커를 사용하여 uri 전달 구현 * feat: 이미지 파일 업로드 api service 구현 * feat: 이미지 파일 업로드 data source 구현 * feat: 이미지 파일 업로드 repository 구현 * feat: 이미지 파일 martipart로 변환해주는 기능 구현 * feat: 이미지 업로드 관련 뷰 수정 * feat: 이미지 파일 업로드 및 api 연결 구현 * style: ktlint format * fix: git conflict 해결 * refactor: 이미지 scaleType 변경 * refactor: string value 컨벤션 적용 * feat: 토큰 반환 시 body가 아닌 cookie로 반환하도록 원상복구 (#223) * feat: 토큰 재발급 API에서 requestHeader로 refreshToken 받도록 수정 (#227) * feat: 토큰 재발급 API에서 body가 아닌 cookie로 토큰 반환 * feat: 회원가입 API도 body가 아닌 cookie로 토큰 반환 * refactor: service 용 dto 명 컨벤션에 맞춰 수정 * feat: 댓글방 일정 수정 API 구현 (#226) * feat: 댓글방 일정 수정 API 구현 * test: 총대가 아닌 참여자가 공모 일정 정보를 수정할 경우 예외 발생 * feat: 댓글방 상태 조회 시 버튼 텍스트 추가 (#229) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 (#222) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 * refactor: 구현 방식 변경 * style: lint적용 * Feature/217 offering status (#230) * feat: 댓글방 상태 조회 api service 구현 * feat: 댓글방 상태 조회 model 및 dto 구현 * feat: 댓글방 상태 조회 datasource 구현 * feat: 댓글방 상태 조회 repository 구현 * feat: 댓글방 상태 조회 api 연결 구현 * style: ktlint 적용 * feat: 댓글방 상태 변경 (#231) * feat: 댓글방 상태 변경 api service 구현 * feat: 댓글방 상태 변경 data source 구현 * Revert "feat: 댓글방 상태 변경 data source 구현" This reverts commit 052691a8de945c60a60586ee66a05a6a3b264217. * feat: 댓글방 상태 변경 data source 구현 * feat: 댓글방 상태 변경 repository 구현 * feat: 댓글방 상태 변경 api 연결 구현 * style: ktlint 적용 * feature: 카카오 로그인 구현 (#235) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * feat: 공모 참여자 목록 조회 API 구현 (#225) * feat: 공모 참여자 목록 조회 API 구현 * test: 실패 테스트 오류 수정 * style: 띄어쓰기 적용 * refactor: MemberEntity를 받도록 변경 * refactor: isParticipant를 구현하여 가독성 개선 * refactor: 총대를 찾을 수 없는 상황의 예외 추가 * refactor: 참여 검증로직을 서비스로 이동 * refactor: 사용하지 않는 메서드 제거 * refactor: 검증 로직 가장 상단에 위치 * refactor: 총대 추출 로직 수정 --------- * refactor: 마감임박순 필터링 쿼리 조건 수정 (#239) * refactor: 마감임박순 필터링 조건 수정 * refactor: 더미 데이터 시간 수정 * fix: 필터링 오류 수정 (#243) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 (#247) * feat: 공동구매 상태 변경 다이얼로그 구현 (#245) * feat: 공동구매 상태 변경 다이얼로그 view 구현 * feat: 공동구매 상태 변경 다이얼로그 Listener 구현 * feat: 공동구매 상태 변경 다이얼로그 연결 및 상태 변경 로직 수정 * test: 테스트 코드 작성을 위한 기본 세팅 (#255) * feat: CoroutinesTestExtension 구현 * feat: Livedata getOrAwaitValue 구현 * feat: InstantTaskExecutorExtension 구현 * feat: TestFixture 생성 * style: ktlint 적용 * feat: 공모글 목록 화면 UI 개선, 공모글 작성에서 낱개 금액이 엔빵 가격보다 저렴할 시 글 작성 막는 기능 구현 (#246) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 구분선을 각각의 아이템의 하단에 넣고 프래그먼트 뷰의 "채팅" 텍스트 밑에 하나 추가 * fix: 텍스트뷰에 font 적용, 마지막 댓글 시간 텍스트를 조금 왼쪽으로 이동 * fix: 낱개 가격 이름을 eachPrice -> originPrice 수정 * fix: 낱개 가격이 엔빵 가격보다 싸면 토스트를 띄우고 글작성을 막는 기능 구현 * fix: 네이티브앱키 로컬프로퍼티로 이동 * refactor: 함수명 변경 * fix: 카카오 계정으로 로그인 후 액티비티 전환하지 않는 문제 수정 * refactor: 사용되지 않는 클래스 삭제 * refactor: 패키지 수정 * refactor: alsong 로그 수정 * refactor: 변수명 수정 * refactor: Manifest의 네이티브앱 키 숨김 * refactor: 로컬프로퍼티의 데이터 형식 수정 * Update android.yml * refactor: alsong 로그 삭제 * ci 빌드 실패가 manifest때문인지 테스트 * refactor: 매니페스트에 앱 키 넣을 수 있게 하는 gradle 설정 수정 * 매니페스트 수정하고 재테스트 * 매니페스트 수정하고 재테스트 * chore: 그래들 수정 * chore: 그래들 수정2 * chore: 그래들 수정3 * chore: 그래들 수정4 * chore: 카카오 계정으로 로그인하는 기능 제외 * feat: 홈화면 테스트 작성 (#257) * chore: mockk의존성 추가 * test: OfferingViewModel 테스트 작성 * style: lint적용 * refactor: stub를 TestFixture로 이동 * test: 댓글방 테스트 코드 작성 (#258) * refactor: 댓글 보내는 함수명 변경 * refactor: 공구 약속 장소 및 시간 캐시 기능 * test: 테스트를 위한 fake repository 구현 * test: 댓글방 viewmodel test 작성 * feat: 댓글방 ActivityTest 작성 * feat: 댓글방 ActivityTest 작성 * style: ktlint 적용 * refactor: test fixture에서 사용하지 않는 것 삭제 * style: ktlint 적용 * feat: GA 모니터링 환경 구축 및 로깅 전략 적용 (#242) * chore: Firebase Crashlytics 의존성 추가 * feat: Firebase 초기화 * feat: FirebaseManager 구현 * feat: 총대가 공구 진행 상황을 다음 단계로 변경했을 때 event 추가 * feat: 로깅 기능 구현 - 검색 - 필터링 - 공모글 클릭 - 공모 참여 * style: lint적용 * feat: 글 작성 완료 시 event 추가 * feat: 로그인 시 event 추가 --------- * test: 공모글 작성 이미지 테스트 코드 작성 (#260) * refactor: 상수 가시성 변경 * feat: test fixture 구현 * feat: fake repository 이미지 업로드 기능 추가 * test: OfferingWriteViewModelTest 이미지 업로드 test 코드 작성 * feat: 로그인 후 홈화면으로 이동해도 로그인 화면이 종료되지 않는 문제 수정 (#261) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 로그인 후 LoginActivity가 종료되도록 수정 * feat: 공모 상세 화면 테스트 작성 (#264) * feat: OfferingDetailViewModel 테스트 작성 * refactor: 테스트 수정 * style: lint적용 * style: lint적용 * feat: 로깅 코드 삽입 (#266) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 * feature: 로깅 샘플 구현 * refactor: 불필요한 코드 제거 * feat: logging 적용 --------- * fix: 마감 임박 필터링 쿼리 수정 (#267) * chore: logback 설정 진행 (#270) * chore: logback 설정 * fix: multipart 요청 필터링 * chore: logback 설정 변경 * chore: pull request ci/cd 닫기 * fix: 이미지 업로드 API의 responseBody가 두 번 뜨는 오류 해결 (#273) * fix: 이미지 업로드 API 두 번 도는 문제 해결 * test: 이미지 업로드 API의 누락된 response field 추가 * refactor: 홈화면 수정 (#271) * refactor: 할인율 마진 추가 * refactor: 공구상태에 대한 문구 수정 * refactor: 클릭 시 최상단으로 이동하는 버튼 구현 * feat: 공모글 작성 화면 테스트코드 작성 (#274) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: 공모글 작성 테스트 구현 * feat: 댓글방 목록 화면 테스트코드 작성 (#276) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: "댓글방 목록을 확인할 수 있어야 한다" 테스트 작성 * feat: pageSize validation 추가 (#279) * feat: pageSize validation 추가 * feat: magic number 추출 * fix: 공모 상세 화면 오류 수정 (#280) * fix: 총대 여부 확인 로직 수정 * fix: 마감 임박 시 보여주는 버튼 수정 * fix: 공모 작성 후 홈화면으로 돌아왔을 떄 목록이 새로고침 되지 않는 오류 수정 * test: 테스트 코드 수정 * style: lint적용 * feat: 댓글방 목록 화면 자동 업데이트 되지 않는 문제 수정, 회원가입 이후 자동으로 로그인되지 않는 문제 수정 (#282) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 라이플사이클 오너 설정 * fix: 회원가입 후 자동으로 로그인 되도록 수정 * chore: change version name (#291) * feat: 카카오 계정 로그인 기능 구현 시 CI가 실패하는 문제 해결 (#296) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * feat: 로그인 화면 리팩토링 (#298) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * refactor: SimpleCookieJar의 패키지 변경(presentation 레이어에서 data레이어의 source 패키지로 이동) * refactor: data store를 관리하는 클래스를 생성하고 이 클래스를 사용하도록 변경 * refactor: 사용하지 않는 의존성과 주석 제거 * refactor: http status code 추가 * refactor: 함수분리 * refactor: ktFormat 적용 * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentRooms) * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentDetail), 사용되지 않게 된 memberId 제거 * refactor: ktFormat 적용 * test: 테스트코드 수정 * refactor: Preferences -> DataStore 이름 변경 * refactor: 채팅방 UI UX 개선 (#303) * feat: 키보드가 아닌 다른 영역을 클릭하면 키보드 내리는 기능 구현 * feat: 뒤로가는 버튼 기능 추가 * feat: 댓글 입력 maxLines 설정 및 maxLength 설정 * style: ktlint 적용 * 필요 없는 코드 제거 * feat: 댓글방 목록에서 자신이 총대인 댓글방의 UI 개선 (#304) * refactor: 댓글방의 자신이 총대인 댓글방 ui 개선 * fix: Binding 클래스 네이밍 수정 * feat: 가로모드, 다크모드 설정 (#305) * refactor: api변경에 따른 리팩토링 (#310) * feat: 로그인 화면 해상도 대응 (#313) * feat: 이미지 업로드 중일 때 로딩 상태 설정 (#317) * feat: 공모 글 작성 ui state 구현 * feat: 로딩 progressbar 생성 * feat: UI 상태에 따른 토스트 메시지 처리 * refactor: 잘못된 입력에 대한 에러 처리 변경 * refactor: 홈화면 리팩토링 (#324) * refactor: textSize dp로 변경 * refactor: 검색 버튼 크기 변경 - 검색 버튼 패딩 추가 - 검색창 끝에 패딩 추가 * refactor: 엔터키를 통해 검색하도록 수정 * refactor: 필터 단일 선택되도록 수정 * style: lint적용 * feat: 댓글방 새로운 기능 GA 연결 (#328) * feat: 댓글방 참여자 확인 Event 구현 * feat: 댓글방 상태 변경 다이얼로그 취소 Event * feat: 참여자가 공구에서 참여 포기 Event 구현 * style: ktlint 적용 * test: 테스트 데이터 수정 (#330) * feat: Fragment GA 모니터링 수집 (#332) * feat: fragment logScreenView 추적 함수 구현 * feat: 각 fragment에서 화면 감지 GA 설정 * feat: 마이페이지 기본 세팅 및 뷰 변경 (#335) * feat: 공모 참여 취소 기능 구현 (#318) * test: 공모 참여 취소 테스트코드 작성 * feat: 공모 참여 취소 기능 구현 * refactor: 불필요한 쿼리 메서드 제거 * style: 불필요한 개행 제거 * refactor: 모집중인 상태가 아닌 경우 공모 참여를 취소할 수 없도록 변경 * refactor: 공모 참여 취소 응답 상태 코드 변경 * refactor: 에러 메시지 명확한 문구로 변경 * refactor: query parameter를 적용해 어떤 공모의 참여를 취소할 것인지 의도를 명확하게 전달하도록 변경 * refactor: 총대 검증 메서드 네이밍 명확하게 변경 * feat: 댓글방 생성 시점 변경 (#319) * feat: 댓글방 생성 시점 변경 * refactor: 불필요한 도메인 OfferingWithRole 제거 * refactor: 불필요한 도메인 CommentWithRole 제거 * refactor: 댓글의 작성자 확인 메서드 추가 * refactor: 댓글방 목록 조회 dto 생성자 추가 * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 (#322) * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 * refactor: 로그인용 dto 분리 및 공통 dto에 prefix로 auth 추가 * feat: valid 어노테이션 추가 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 (#323) * refactor: 메서드명 구체적으로 변경 * refactor: 변수명 구체적으로 변경 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 * docs: todo 추가 * refactor: 함수명 통일 * feat: 공모자 여부 필드명 변경 * feat: 댓글방 상태 조회 API 확장 (#325) * feat: 댓글방 상태 조회 API 확장 * refactor: 댓글방 관련 로직 댓글 도메인으로 이동 * feat: LoggingFilter에서 던지는 유효하지 않은 요청에 대한 예외 처리 * refactor: 댓글 관련 엔드포인트 수정 * feat: 댓글방 정보 조회 시 조회 권한을 가진 사용자인지 검증 * refactor: 댓글방 상태 확인 로직 도메인으로 이동 * feat: 상태 변경을 시도하는 사용자가 총대인지 검증 * refactor: 댓글 목록 조회 엔드포인트 수정 * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 (#327) * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 * refactor: Response depth 줄이기 및 DTO 생성자 작성 * fix: imminent 필터 버그 해결 (#337) * fix: 커스텀 필터로 인해 h2-console 접속 깨지는 이슈 해결 (#339) * feat: 마이페이지 기능 구현 (#341) * feat: 마이페이지 닉네임 기능 구현 * feat: 로그아웃 로직 구현 * feat: url 연결 로직 구현 * feat: 필요없는 기능 삭제 * style: ktlint 적용 * feat: 공모 테이블에 할인율과 상태 필드 추가 (#342) * refactor: Condition과 Status 이름 변경 * refactor: 사용하지 않는 DTO 제거 * feat: OfferingEntity에 칼럼 추가 * feat: 공모 거래 날짜 필드 이름 변경 (#348) * fix: 상세화면에서 홈화면으로 갔을 때 상태 변경 안되는 오류 수정 (#343) * refactor: 공모상세페이지 Activity -> Fragment로 리팩토링 * fix: 페이지네이션 및 상태변경 미적용 오류 해결 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 주석 제거 및 상수화 * refactor: livedata 자료형 변경 * refactor: progressbar위치 수정 * refactor: lifecycleScope사용 리팩토링 * refactor: adapter에서 전체 아이템이 아닌 특정 아이템만 notify하도록 리팩토링 * refactor: API변경에 따른 대응 (#352) * refactor: api대응 * refactor: api변경에 따른 테스트 수정 * feat: 공모글 작성 화면 ux 개선 (#344) * fix: 각 항목의 설명을 place holder로 이동 * fix: 필수와 선택 항목의 프래그먼트 분리 * feat: 버튼이 항상 보이도록 수정 * fix: 가격과 총원은 숫자만 입력받도록 변경 * fix: 패딩 수정 * fix: ui 수정 * fix: 도메인 변경에 따른 deadline -> tradeDate 수정 * feat: 필수 항목을 모두 입력하면 선택 항목 화면으로 이동하는 기능 구현 * refactor: ktFormat 적용 * refactor: shared viewModel 사용, 미필수 항목을 미필수 입력 화면으로 이동 * refactor: 프래그먼트 이름 변경 * feat: 입력 숫자의 글자수와 라인수 제한 기능 구현 * fix: 총원이 -1이하로 떨어지는 버그 수정, 공동구매 텍스트 띄어쓰기 제거 * fix: 할인율, 엔빵 금액이 유효하지 않을 때는 "-"로 뜨도록 변경 * fix: 공모를 게시하면 필수, 선택 화면 모두 종료되도록 수정 * fix: 날짜 시간 픽커를 날짜만 선택하는 픽커로 변경 * refactor: ktFormat 적용 * refactor: 바인딩어댑터의 파라미터를 nullable하게 수정 * test: 테스트코드 수정 * feat: 낱개 가격의 place holder로 현재 엔빵 금액을 보여주는 기능 구현 * feat: 내용의 최대 글자수와 현재 글자수를 보여주는 기능 구현 * refactor: ktFormat 적용 * refactor: 공모글 작성시 memberId를 보내지 않도록 변경 * fix: 총원 최대 4자리에서 3자리까지만 입력받을 수 있도록 변경 * fix: deadline -> meetingDate 네이밍 수정 * fix: 공모글 작성 후 작성 화면의 입력값이 초기화되지 않는 버그 수정 * refactor: 네이밍 수정(eachPrice -> originPrice) * refactor: 네이밍 수정(individualPrice -> originPrice) * fix: 내용의 현재 글자수 색이 메인컬러가 되지 않는 문제 수정 * refactor: 프래그먼트 종료될 때 바인딩 해제하도록 수정 * refactor: id가 없는 뷰의 id 추가 * refactor: 함수 분리 * fix: 내용 옆의 * 제거 * fix: GA 이벤트 이름 변경(공모글 작성 - 필수 화면에서의 이벤트임을 명시함) * refactor: og 태그 추출 기능 수정 (#349) * refactor: crawler 패키지 이동 * feat: naver api 클라이언트 추가 refactor: 사용하지 않은 기존 og image 크롤러 명칭 변경 * feat: html 크롤링 방식과 naver api 방식을 조합하는 Extractor 구현 * fix: OfferingService ProductImageExtractor 추상화 * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 (#358) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 * test: 테스트코드 수정 * refactor: 공모글 목록 조회 필터링 수정 및 추가 (#356) * refactor: 마감임박순 필터링 이름 마감임박만으로 변경 * refactor: 필터링 쿼리 수정 * feat: "참여가능만" 필터링 기능 구현 * feat: "참여가능만" 필터링 기능 연결 * fix: 쿼리 내 불필요한 파라미터 제거 * refactor: 할인율이 null일 경우 높은할인율 필터링 대상에서 제외 * feat: 참여가능만 필터링 전략 클래스 추가 * feat: 공모 목록 조회 API 응답값 변경 * fix: 높은 할인율 단위 변경 및 last-id 필터링 로직 수정 * style: 주석 제거 --------- * refactor: 할인율 계산 로직 수정 (#359) * refactor: 할인율 계산 로직 수정 * refactor: 소수점 둘째 자리에서 반올림하도록 변경 * test: 할인율 계산 로직 * fix: 할인율 단위 백분율로 수정 --------- * feat: 총 모집 인원 수 최댓값 설정 (#361) * fix: 필터 오류 수정 (#362) * fix: 필터 오류 수정 - '참여가능만'필터 분기처리 제거 * chore: 주석 제거 * feat: API 스펙 변경에 따른 대응 (#364) * feat: 댓글 목록 조회 api 스펙 변경에 따른 대응 * feat: 댓글방 정보 조회 api 스펙 변경에 따른 대응 * feat: 공모 일정 조회 api 스펙 변경에 따른 대응 * feat: 댓글 상태 변경 api 스펙 변경에 따른 대응 * test: api 스펙 변경에 따른 test 코드 변경 * style: ktlint 적용 * feat: remote dto package 분리 * feat: 자동 확정 기능을 위해 스케줄러 적용 (#363) * chore: todo 추가 및 메서드명 변경 * feat: Scheduled 어노테이션 추가 및 Scheduler 분리 * test: ServiceTest 환경 구축 * feat: offeringStatus 변경 로직 추가 * refactor: 수동 확정 로직 추가 및 코드 스타일 수정 * refactor: 자동 확정 로직을 조회에서 Scheduled로 이동 * fix: 마감임박 설정 기준 내일로 변경 --------- * fix: 공모 작성 후 홈화면 돌아올 때 새로 작성한 글이 보이지 않는 오류 수정 (#369) * feat: Access Token, Refresh Token을 data store에 저장하는 기능 구현 (#372) * feat: 앱 재시작 시 토큰을 데이터스토어에서 꺼내 사용하는 기능 구현 * feat: 로그인이 이미 되어있다면 로그인 화면을 건너뛰는 기능 구현 * feat: 로그아웃 기능 구현 * fix: 마이페이지 화면으로 넘어가면 바텀네비게이션이 사라지는 버그 수정 * fix: 데이터스토어에서 토큰이 꺼내지지 않는 버그 수정 data store에서 토큰을 꺼내는 코루틴 비동기 작업이 끝나기 전에 함수를 종료해 버려서 생기는 버그였습니다. * refactor: ktFormat 적용 * refactor: startActivity 함수를 LoginActivity가 동반객체로 갖고 있도록 변경 * refactor: 함수명과 event명 변경 추가로 GA위치가 조금 잘못된 점이 있어서 수정했습니다. * feat: 공모 상세 화면 추가 기능 반영 (#375) * feat: 신고하기 기능 구현 * feat: 물품 링크가 없으면 보여지지 않도록 구현 * refactor: 마감 시간에서 거래 날짜로 리팩토링 * feat: 이미 참여한 공모게시글에서 채팅방으로 이동하는 기능 구현 * fix: 댓글방 목록의 마지막 댓글방이 보이지 않는 문제 수정 (#376) * fix: 리사이클러뷰 레이아웃의 크기가 화면 밖에 벗어나지 않도록 수정 * fix: 리사이클러뷰 레이아웃의 맨 밑에 구분선 하나 추가 아래로 땡겼을 때 구분선이 사라져버리는게 보기 안좋아서 추가했습니다 * refactor: 코트 포맷 적용 (컨트롤 알트 L) * feat: isManualConfirmed 제거 및 도메인 로직 확인 (#377) * refactor: isManualConfirmed 칼럼 삭제 및 관련 로직 분리 * refactor: 더미 데이터 수정 --------- * feat: API 별 권한 확인 로직 추가 (#371) * feat: 권한 확인 로직 추가 * feat: 인증 필터 적용 * refactor: 더미 데이터 칼럼 위치 변경 (#382) * refactor: 홈화면 api필드 추가에 따른 대응 (#381) * refactor: dto필드 추가 * fix: 상태 변경 오류 해결 * fix: 필터 선택 또는 검색상태일 때 공모 작성 후 나오면 목록 안보이는 오류 수정 * refactor: 세부 주소 api에서 받아오도록 변경 * style: lint적용 * fix: API 문서에 접근할 수 없는 현상 해결 (#384) * fix: API 문서에 접근할 수 없는 현상 해결 * style: 신뢰할 수 있는 URL 개행 수정 * feat: 공모 목록에서 동을 보여주는 기능 구현 (#386) * feat: 공모 단건 조회 API 구현 (#388) * feat: 공모 상세 조회 API 엔드포인트 변경 * feat: 공모 단건 조회 API * style: 공모 관련 API 순서 변경 * test: 불필요한 공모글 생성 코드 제거 * test: 공모 단건 조회 서비스 테스트 * refactor: 상태변경 리팩토링 (#389) * refactor: 공모 상세 조회 api변경 대응 * refactor: 공모 상태 변경 리팩토링 * refactor: 리팩토링에 따른 테스트 수정 * chore: 불필요한 로그 제거 * fix: 댓글 입력 후 뒤로가기 시 최근 댓글이 반영되도록 수정 (#397) * chore: JAR 파일에 OAS 파일 누락되는 이슈 해결 및 중복 task 제거 (#391) * chore: 중복되는 task 제거 * chore: cicd 범위 조정 * fix: 참여자 목록 조회 API에서 totalCount 반환하지 않는 이슈 해결 (#400) * feat: 댓글방 참여자 확인 API 연결 (#401) * feat: 참가자 정보를 가져오는 api service 구현 * refactor: 필요없는 코드 삭제 * feat: 참여 관리 datasource 구현 * feat: 참여자 domain 모델 구현 * feat: 참여를 관리하는 repository 구현 * feat: 참여자 목록을 보여주는 recycler view 연결 및 구현 * refactor: 더보기 버튼 수정 * feat: 필요없는 리소스 파일 삭제 및 상태 기본 이미지 변경 * refactor: 약속 장소 및 시간 ui model 을 사용하여 관리 * refactor: 댓글방의 정보를 불러오는 로직 ui model을 사용하여 관리 * refactor: ui model 변환 로직 변경 * feat: 공동구매 참여 인원 확인 기능 구현 * feat: 신고하기 폼 연결 구현 * test: 코드 변경에 따른 테스트 코드 수정 * style: ktlint 적용 * refactor: xml id 추가 * feat: 댓글방 공동구매 나가기 API 연결 (#402) * feat: 공동구매 나가기 기능 api service 구현 * feat: 공동구매 나가기 기능 data source 구현 * feat: 공동구매 나가기 기능 repository 구현 * feat: 공동구매 나가기 기능 연결 * style:ktlint 적용 * fix: /auth/refresh endpoint accessToken 검증 예외 추가 (#407) * refactor: 더미 데이터 정합성 확보 (#406) * refactor: 더미 데이터 정합성 확보 * refactor: 추가된 칼럼 반영 * feat: CallApiHandler 구현 (#403) * feat: CallApiHandler 구현 * refactor: CommentRoomsDataSource 수정 * feat: CommentRemoteDataSourceImpl 에러핸들링을 통해 수정 * feat: 에러 핸들링에 따른 DataSource 리팩토링 - OfferingDetailDataSource - OfferingRemoteDataSource * feat: ParticipantRemoteDataSourceImpl 에러핸들링을 통해 수정 * style: ktlint 적용 * refactor: AuthRemoteDataSource 수정 * feat: Result의 map 과 getOrThrow 함수 생성 * feat: 에러 핸들링에 따른 Repository 리팩토링 - OfferingDetailRepository - OfferingRepository * refactor: Result 변경에 따른 레포지토리 수정 (AuthRepository, CommentRoomsRepository) * feat: 에러 핸들링에 따른 CommentDetailRepository 리팩토링 * feat: 에러 핸들링에 따른 ParticipantRepository 리팩토링 * feat: 에러 핸들링에 따른 viewmodel 리팩토링 - OfferingViewModel - OfferingDetailViewModel * refactor: 에러 핸들링에 따른 LoginViewModel 리팩토링 * refactor: 에러 핸들링에 따른 CommentRoomsViewModel 리팩토링 * refactor: 토큰 리프레쉬 후 다시 함수 호출하도록 추가 * feat: 에러 핸들링에 따른 CommentDetailViewModel 리팩토링 * refactor: 에러 핸들링에 따른 OfferingWriteViewModel 리팩토링 * refactor: 공모 목록 토큰 리프래시 적용 * fix: 잘못된 코드 수정 * refactor: 필요없는 주석 제거 * refactor: 공모 목록 리팩토링 * fix: 리빌드시 쿠키가 제대로 저장되지 않는 현상 수정 * refactor: 필요없는 코드 삭제 및 상수화 추가 * test: 에러핸들링에 따른 FakeAuthRepository, OfferingWriteViewModelTest 수정 * refactor: ktFormat 적용 * test: 코드 변경에 따른 Fake Repository 변경 * test: CommentDetailViewModelTest 코드 수정 * style: ktlint 적용 * refactor: 가독성 개선(에러 로그 함수명 추가, Success가 Error보다 위에 나오도록 수정) * refactor: 불필요한 로그 제거 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 람다 넘겨주는 방식 수정 * style: lint 적용 * test: 테스트코드 수정 --------- * feat: proguard를 사용한 난독화 적용 (#413) * chore: 환경에 따른 yml 파일 분리 (#411) * chore: 환경 별로 yml 파일 분리 * chore: 불필요한 yml 설정 제거 * fix: 공구 상세 페이지 오류 해결 (#417) * fix: 바로가기 클릭되지 않는 오류 수정 * refactor: 주소 표시할 때 최대 2줄까지 그리고 넘어갈 시 말줄임 나오도록 수정 * refactor: 공모 목록, 공모 상세 에러 핸들링 (#418) * refactor: 공모 목록에서 401에러를 제외하고는 에러코드 올 시 빈화면 보여주도록 에러핸들링 수정 * refactor: 필터및 업데이트된 공모 목록 가져오는 로직 에러핸들링 수정 - 400: 토스트 메시지 띄어줌 - 401: refresh - 그외에는 로그로 에러 코드를 보여줌 * refactor: strings네이밍 통일 * refactor: 공모 상세 에러 핸들링 수정 * refactor: strings정리 - offering_detail부분 정리 * feat: 카카오 로그인 중 사용자 정보 확인 로직을 안드로이드에서 백엔드로 이관 (#404) * feat: 카카오 로그인 API 구현 * feat: providerId를 loginId로 수정 * feat: 소셜 로그인 시 랜덤 생성된 비밀번호 사용 * refactor: 불필요한 api 제거 * test: 로그인 로직 변경 * test: MemberFixture 불필요한 함수 제거 및 통일 * refactor: 불필요한 정보 제거 * feat: 카카오 로그인 에러 핸들러 추가 * feat: 민감 정보 로깅에서 제외 --------- * feat: cookie 관련 예외 처리 (#409) * refactor: 더미 데이터 http 추가 (#422) * fix: 더미데이터 정합성 맞추기 (#425) * feat: 로그인 api 변경 반영 (#426) * feat: 카카오 로그인 후 총대마켓 서버로 email을 보내던 방식에서 카카오 access token을 보내는 방식으로 변경 * feat: login과 signup을 하나로 api로 통합된 것 반영 * refactor: ktFormat 적용 * refactor: 테스트코드 수정 * feat: 로깅 시 UUID가 아닌 회원 번호가 기록되도록 변경 (#428) * feat: logging 시 memberId가 나오도록 기능 추가 * feat: logging 시 memberId 및 identifier가 함께 나오도록 변경 * refactor: lombok getter 적용 * feat: Spring Timezone KST로 설정 (#430) * chore: Dockerfile 타임존 변경 (#432) * fix: Offering 목록 조회 시 NPE 해결 (#434) * refactor: 에러 핸들링 리팩토링 (#436) * feat: 리프레시 토큰 만료 시 데이터스토어를 비우고 로그인 화면으로 이동하는 기능 구현 (#438) * feat: 댓글방 에러 헨들링 (#439) * refactor: refresh시 401이 오는 경우에 대한 에러핸들링 추가 (#441) * chore: 버전 업데이트 (#443) * refactor: 외래키 필드 notnull 조건 추가 (#445) * chore: prod CI/CD 구축 (#423) * chore: 환경 별로 yml 파일 분리 * chore: 운영 서버 CI/CD 스크립트 작성 * chore: 운영 환경 내 swagger 문서 제거 * chore: 운영 환경 포트포워딩 명령어 제거 * chore: prod ci/cd 스크립트 트리거 추가 * chore: prod ci/cd 스크립트 트리거 변경 * chore: prod ci/cd 스크립트 트리거 path 구체화 * chore: prod ci/cd 스크립트 docker 실행 명령어 오타 수정 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev 및 prod ci/cd 스크립트 data.sql 실행 비활성화 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev script test --------- * chore: prod 불필요한 트리거 주석 처리 (#447) --------- Co-authored-by: Namyunsuk <84739562+Namyunsuk@users.noreply.github.com> Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: 채현 Co-authored-by: SCY Co-authored-by: alsong <138569524+songpink@users.noreply.github.com> Co-authored-by: masonkimseoul <87306418+masonkimseoul@users.noreply.github.com> Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: masonkimseoul Co-authored-by: fromitive Co-authored-by: Namyunsuk Co-authored-by: songpink * merge: main to develop-AN (v.1.1.0) * feat: 게시글 상세 화면 구현 (#8) * feat: 게시글 상세 화면 레이아웃 작성 * feat: Data layer코드 작성 * refactor: dto패키지 분리, dto에 serialName추가 * refactor: 도메인 모델 수정 - 가변에서 불변으로 변경 - 사용하지 않는 메서드 제거 * refactor: 공통으로 사용되거나 사용될 수 있는 확장함수를 별도의 파일로 분리 * style: lint 적용 * refactor: 메서드명 컨벤션 적용 * refactor: request Dto에 SerialName적용 * refactor: 메서드명 수정 * feat: 도메인 추가 (#15) * feat: BaseTimeEntity 추가 * feat: Member Entity 추가 * feat: Offering Entity 추가 * feat: OfferingMember Entity 추가 * feat: Comment Entity 추가 --------- * feat: BottomNavigation 구현 (#16) * chore: jetpack navigation 라이브러리 추가 * feat: 필요한 바텀 네비게이션 리소스 추가 * feat: bottom navigation fragment 추가 * feat: bottom navigation graph 구현 * refactor: 컨벤션에 맞게 id 수정 * feat: 공동구매 상세 조회 기능 구현 (#18) * chore: h2 환경설정 추가 * docs: http client 추가 * refactor: entity 접미어 적용 * chore: dummy data 추가 * docs: http client 값 변경 * refactor: repository 와 domain 패키지 분리 * feat: 공동구매 상세 조회 API 구현 * refactor: entity 접미어 적용 * style: 클래스 컨벤션 적용 * chore: h2 console 설정 제거 * refactor: OfferingCondition enum값 결정로직을 enum 안으로 이동 * feat: 홈화면, 마이페이지 화면 레이아웃 작성 (#19) * refactor: FragmentContainer width 속성 수정 * feat: 홈 화면 레이아웃 작성 * feat: 마이페이지 화면 레이아웃 작성 * fix: 플로팅 버튼이 홈에서만 보이도록 수정 * refactor: 리소스 네이밍 컨벤션에 맞게 수정 * feat: API 문서화 적용 (#23) * chore: springdoc-openapi 의존성 추가 * chore: springdoc 설정 추가 * feat: SwaggerConfig 파일 추가 * feat: 공모 상세 조회 API 문서화 --------- * fix: 공모 상세 조희 API의 price 필드 자료형 변경 및 memberId 필드 추가 (#28) * fix: 상세조회 API 금액 필드 자료형 변경 * fix: memberId 추가 * 내가 쓴 글인지 아닌지 확인 위해 --------- * chore: 백엔드 CI 및 도커 파일 작성 (#27) * chore: actions 적용 브랜치 설정 (#30) * chore: actions 적용 브랜치 설정 * chore: path 및 ref 태그 제거 * chore: working-directory 태그 추가 * chore: Dockerfile jar 경로 수정 * feat: 댓글방 목록 구현 (#26) * feat: 댓글방 목록 UI 구현 * fix: 구분선을 ImageView에서 View로 변경 * feat: 댓글방 목록 도메인 모델 구현 * feat: 댓글방 어답터 구현 * feat: "채팅" string 추가 * refactor: 불필요한 코드 제거 * fix: xmls 중복 속성 제거 * refactor: 댓글방 클래스들을 comment 패키지로 분리 * refactor: 컬러와 폰트 사이즈를 values 파일로 분리 * feat: 공모 목록 조회 기능 구현 (#35) * feat: 공모 목록 조회 API 구현 * docs: 공모 목록 조회 API http client에 추가 * fix: 공모 상세 조회 API의 status 필드를 condition으로 명칭 변경 * feat: 공모 목록 조회 API의 isClosed 필드 이름을 isOpen으로 변경 * feat: 댓글방 디테일 화면 구현 (#32) * feat: font 설정 * feat: vector 이미지 추가 * feat: 채팅 아이템 뷰 구현 * refactor: 컨벤션에 맞게 네이밍 수정 * feat: 댓글 입력 edit text 구현 * chore: 백엔드 CD 스크립트 작성 (#34) * chore: 백엔드 CD 스크립트 작성 * chore: 도커 백그라운드로 실행 * chore: 도커 설정 및 트리거 설정 변경 * chore: 도커 이미지 제거 로직 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 강제 제거하도록 수정 * chore: gradle 캐싱 로직 추가 (#39) * chore: gradle 캐싱 로직 추가 * chore: 이벤트 트리거 조건 수정 * feat: 공모 참여하기 기능 구현 (#40) * fix: BaseTimeEntity 적용 오류 수정 * feat: 참여하기 API 구현 --------- * feat: 공모 상세 조회 API에 참여자 목록 필드 추가 (#42) * feat: 공모 상세 조회 API의 request에 memberId 필드 추가 (#45) * feat: 공모 참여 API의 불필요한 응답값 전부 제거 (#48) * feat: 공모 참여 API의 불필요한 반환값 제거 * chore: 자주 쓰는 h2 console enabled 설정 주석 처리 * feat: 이미 참여한 공모에 참여 못하게 예외 처리 (#51) * feat: 공모 상세 페이지 API 연결 (#46) * build: 불필요한 의존성 제거, properties관련 코드 작성 * refactor: base_url코드상에서 제거 * feat: api수정에 따른 필드 변경 및 네이밍 반영 * refactor: 네이밍 변경 * refactor: OfferingDetail의 변경, mapper변경 * refactor: service분리 * refactor: DataSource, Repository분리 * refactor: API변경에 따른 리팩토링 * feat: 공모 상세 조회 기능 구현 * refactor: 참여하기 api변경에 따른 data, domain 코드 수정 * feat: 공모 상세 페이지 참여하기 기능 구현 * feat: 공모 상세 화면에서 이미지를 불러올 수 없을 시 기본이미지를 보여주는 기능 구현 * feat: 게시물 상세 화면 폰트 적용 * style: lint적용 * refactor: 액티비티 destroy시 binding해제하도록 코드 추가 * refactor: glide옵션 변경 - 에러 발생 시 보여줄 이미지 - url이 null일 시 보여줄 이미지 * refactor: viewModel에 custom getter추가 * fix: 내용이 짧을 시 뒷 배경이 회색으로 보이는 버그 수정 * fix: 참여하기 버튼을 눌렀을 시 텍스트가 바뀌지 않는 버그 수정 * feat: 테스트 데이터 다양화 (#52) * refactor: 공모 엔티티에 currentCount 필드 추가 (#55) * feat: 댓글 작성 API 구현 (#57) * feat: 댓글방 내 공모 일정 조회 기능 구현 (#58) * feat: 댓글방 내 공모 일정 조회 기능 구현 * refactor: 공모 일정 조회 api 명세 변경 --------- * refactor: common 패키지명을 global로 변경 (#61) * chore: 안드로이드 CI 파일 작성 (#63) * feat: 댓글 목록 조회 API 구현 (#66) * chore: build CI 작업을 위한 manifest 파일 수정 (#65) * chore: 알람 권한 추가 * chore: local properties 속성 추가 * chore: local properties null 체크 로직 추가 * chore: buildConfigField null 체크 * style: lint 적용 * chore: secret 값 설정 * fix: secret 값 오류 수정 * fix: 문법 오류 수정 * chore: 경로 수정 * chore: 문법 수정 * style: lint 적용 * feat: 댓글방 목록 조회 API 구현 (#70) * feat 댓글방 접히는 공지 뷰 구현 (#72) * chore: manifest에 CommentDetailActivity 추가 * feat: BindingAdatper을 사용하여 접힐 때 애니메이션 적용 및 픽셀 변환 * feat: viewmodel 구현 및 click 마다 접히고 펴지는 로직 구현 * style: ktlint 적용 * refactor: binding adpater을 사용하여 가시성 변경 * refactor: 댓글방 및 댓글 목록 조회 서비스 계층 (#78) * fix: 댓글방 목록 조회 시 가장 최근 댓글 조회 (#80) * feat: 홈화면 API 연결 (#74) * refactor: API변경에 따른 data, domain 코드 변경 * feat: 공모 목록 기능 구현 * refactor: 함수 분리 * style: lint적용 * style: font 적용 * fix: 시간순 정렬 쿼리 추가 (#83) * chore: 더미 데이터 추가 (#87) * feat: 댓글방 목록 API 연결 (#82) * feat: bottom navigation fragment 추가 * feat: vector 이미지 추가 * feat: 댓글방이 없으면 "채팅 목록이 없어요" 라는 텍스트뷰와 이미지뷰를 띄우는 기능 구현 * feat: 댓글방 띄우는 기능 구현 * test: 댓글방 UI 테스트 작성 * refactor: 테스트 클래스명 수정 * refactor: 줄바꿈 수정 * feat: 댓글방 API 서비스 구현 * refactor: API 명세에 따라 도메인 모델 수정 * feat: API 연결 * refactor: API명세에 따라 데이터바인딩 변수명 수정 * feat: 댓글방 목록 API 연결 * refactor: ktlint Format 적용 * refactor: 메모리 누수 방지를 위해 fragment가 destroy 될 때 _binding을 null로 설정 * refactor: 어답터를 방어적복사 하지 않아도 되어서 수정 * refactor: 채팅방이 없다는 이미지뷰를 띄워주는 방식 수정(바인딩 어댑터 수정) * refactor: 함수분리 * refactor: ktFormat 적용 --------- * feat: 댓글방 접히는 공지 API 연결 (#85) * feat: 미팅 일정 API 연결을 위한 data layer 구현 * feat: 미팅 일정 API 연결을 위한 domain layer 구현 * feat: 미팅 일정 API 연결을 위한 presentation layer 구현 * style: ktlint 적용 * feat: 공동 구매 제목 databinding 적용 * refactor: 변수명 수정 * fix: 펼치기 접기 버튼 로직 반대로 수정 * style: ktlint 적용 * chore: 더미 데이터 바로가기 url 수정 (#93) * feat: 공모 상세 페이지 기능 추가 (#94) * chore: 마이페이지 닉네임 임시로 지정 * feat: 바로가기 기능 구현 * feat: 참여버튼 클릭 시 댓글방으로 가도록 기능 구현 * feat: 신고하기 이미지 추가 * style: lint적용 * refactor: 불러오는 공모 페이지 사이즈 변경 * refactor: 댓글 도메인 코드 리팩터링 (#96) * refactor: 로그인 멤버 변수명 변경 * refactor: JPQL 쿼리 컨벤션 및 멤버로 공모 조회 메서드명 변경 * refactor: 최근 댓글 응답 클래스명 변경 * refactor: 컨트롤러 및 서비스 API 순서 변경 * refactor: 로그인 사용자 유효성 검증 * feat: 댓글방 댓글 작성 api 연결 (#95) * chore: windowSoftInputMode 추가 * feat: post comment api service 구현 * feat: post comment DataSource 구현 * feat: post comment Repository 구현 * feat: post comment Presentation 구현 * chore: 더미 데이터 시간 변경 (#100) * feat: 댓글방 입장 기능, 본인이 총대인 방은 다르게 보이는 기능 구현 (#99) * feat: 댓글방의 마지막 댓글 시간을 띄우는 기능 구현 * feat: 자신이 총대인 댓글방을 표시하는 기능 구현 * feat: 댓글방 목록을 클릭해 댓글방 상세로 이동하는 기능 구현 * test: UI테스트 수정 * refactor: 클릭시 id 뿐만 아니라 title도 받아오는 방식으로 수정 * refactor: 오전/오후와 시간을 텍스트뷰에 띄우는 바인딩 어댑터를 DateTimeFormatter의 기능을 사용하는 것으로 수정 * refactor: memberId를 local.properties의 token을 가져다 쓰는 것으로 변경(임시 조치) * refactor: 댓글방 목록의 시간을 띄우는 바인딩 어댑터의 속성명을 수정함 * refactor: 데이터바인딩 variable 변수명을 구체적으로 수정, 일관성을 위해 앞에 `on` 붙임 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정(빠트린것 수정함) * feat: 전반적인 예외 처리 (#103) * feat: 예외 처리 핸들러 추가 * feat: Offering 예외 처리 코드 추가 * feat: Comment 예외 처리 코드 추가 * feat: Member 예외 처리 코드 추가 * feat: OfferingMember 예외 처리 코드 추가 * feat: Offering 예외 처리 상세 코드 추가 * feat: 에러 코드 적용 * feat: 도메인 검증 로직 * feat: DTO 검증 로직 --------- * feat: swagger와 restdocs 연동 (#104) * chore: swagger ui 정적 파일 설치 및 static routing 세팅 * chore: restdocs-api-spec을 이용한 OAS 생성 * chore: swagger ui 정적 파일을 swagger-ui 디렉토리로 이동 * chore: swagger ui 정적 파일 및 static routing 세팅 제거 * chore: 생성된 OAS 파일을 Swagger 디렉터리로 복사하는 스크립트 작성 * chore: openapi3 yaml 파일 gitignore 처리 * chore: static routing 세팅 다시 추가 openapi3.yaml을 사용하기 위함 * test: RestAssured RestDocs 테스트 코드 작성 * test: 공모 목록 조회 API 문서화 * test: 공모 일정 조회 API 및 공모 참여 API 문서화 * test: 댓글 관련 API 문서화 * docs: 논의된 TODO 제거 * refactor: swagger 어노테이션 제거 * chore: 개발 API 서버 목록 설정 --------- * refactor: 에러메시지 필드명 변경 (#108) * fix: restdocs 관련 테스트 실패 이슈 해결 (#106) * chore: cicd 테스트 * chore: 테스트 위해 actions 범위 조정 * chore: 배포 스크립트 띄어쓰기 오타 수정 * chore: 빌드 캐싱 제거 * chore: logging * chore: --warning-mode all 옵션 줘서 gradle 호환 무시하도록 설정 * fix: status 달라서 실패하는 테스트 수정 * chore: actions 범위 수정 * chore: action 범위 수정 * chore: test용 static 파일 추가 * chore: static 하위 폴더를 jar 파일에 포함하도록 설정 * chore: swagger-ui 하위 폴더 제거 * chore: task 순서 조정 * chore: build 스크립트 수정 * chore: 불필요한 설정 변경 제거 * chore: clean build 대신 clean bootJar 사용 * chore: clean, build 각각 하도록 변경 * chore: test 까지 두 번 돌리도록 수정 * chore: openapi3까지 두 번 실행하도록 수정 * chore: copyOasToSwagger 까지 두번 실행하도록 수정 * chore: actions 활성화 범위 수정 * fix: 댓글방 목록 조회 시 참여자 수 조건 추가 (#111) * fix: 댓글방 조회 테스트 수정 (#113) * feat: 홈 화면 무한 스크롤 기능 구현 (#109) * build: pagination라이브러리 추가 * feat: 홈 화면 무한 스크롤 기능 구현 * fix: 마지막 댓글 response를 nullable하게 수정 (#115) * fix: 마지막 댓글 response를 nullable하게 수정 * refactor: ktFormat 적용 * feat: 댓글방 댓글 조회 api 연결 (#116) * feat: dto 및 mapper 구현 * feat: 댓글방 목록 service 구현 * feat: 댓글방 목록 data source 구현 * feat: 댓글방 목록 repository 및 model 구현 * feat: 댓글방 목록 view type을 활용한 recyclerview 구현 및 데이터 바인딩 * feat: polling 기능 구현 * feat: 댓글 스크롤 구현 (새로운 댓글이 생길시 스크롤 아래로) * feat: 총대와 다른 참가자 이미지 리소스 파일 * feat: 댓글방 디테일 공동 구매 상태별 관리 (#117) * feat: 공동구매 상태 관리 리소스 파일 * feat: 공동구매 상태를 관리하는 enum class 구현 * feat: 데이터바인딩을 사용하여 공동 구매 상태 뷰 업데이트 구현 * style: ktlint 적용 * feat: 공동구매 상태 관리 리소스 파일 추가 * fix: 이미지 링크 임시 수정 (#119) * fix: 이미지 링크 수정 (#120) * refactor: 네이밍 수정 (#123) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 (#125) * refactor: 뷰모델 팩토리 방식 변경 (#130) * refactor: 뷰모델 팩토리를 뷰모델의 동반객체로 이동 * style: lint적용 * refactor: Service분리 (#132) * refactor: service분리 * refactor: 패키지명 변경 * style: lint적용 * feat: 공모글 작성 UI 구현 (#134) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 뷰 구현 * fix: 뷰 수정사항 반영 * fix: @+id로 참조하는 부분을 수정 * fix: drawable의 네이밍에 where을 추가 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 (#136) * feat: 참여자 목록 drawer에 필요한 리소스 파일 추가 * refactor: 채팅 text gravity 수정 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 * style: ktlint 적용 * refactor: drawer early return 하는 방식으로 변경 * refactor: ivMore -> ivMoreOptions으로 네이밍 변경 * feat: 공구 참여자 item view 및 댓글방 view 사용자 친화적으로 수정 * chore: CI 빌드 스크립트 중 중복되는 task 제거해 성능 개선 (#128) * chore: jar태스크 비활성화하고 bootJar 태스크로만 JAR 파일 생성 * chore: cicd 범위 조정 * feat: 공모 작성 API 구현 (#139) * feat: 공모 작성 API 구현 * refactor: create를 save로 변경 * refactor: dto entity 매핑로직을 dto로 이동 * refactor: controller request 매개변수 명 컨벤션 적용 --------- * refactor: 공모에 저장하는 주소 값 구체화 (#141) * refactor: 공모에 저장하는 주소 값 구체화 * chore: github-action 스크립트 수정 * chore: CI/CD test 설정 추가 * chore: static/swagger-ui 폴더 추가 * chore: 설정 원상 복구 * chore: ci/cd 범위 수정 --------- * feat: 홈화면(공모목록) UI 추가 구현 및 상태 변경 대응 (#142) * feat: 공모의 상태 변경이 반영되도록 기능 구현 * feat: 공모 목록 ui변경 * feat: 필터 ui추가 * feat: API변경에 따른 DTO수정 * style: lint적용 * feat: resource추가 * refactor: ui위치 수정 * chore: 불필요한 괄호 제거 * refactor: item 수직 정렬 * feat: 댓글방 메시지 조회 시 commentId 필드 추가 (#150) * feat: OG 태그 크롤링 API 구현 (#148) * feat: OG 태그 크롤링 API 구현 * refactor: OG 태그 크롤링 API 엔드포인트 수정 --------- * refactor: 제품 코드와 API 문서 동기화 (#153) * refactor: API 문서 개선 (#157) * refactor: 댓글 작성 시 성공 상태 코드 변경 * refactor: 요청 필수 상태 설명 추가 --------- * feat: s3 이미지 업로드 API 구현 (#147) * feat: s3 이미지 업로드 API 구현 * chore: cicd 액션 범위 수정 * fix: 이미지 업로드 경로의 특수문자 제거 * chore: yml multipart 설정 추가 * chore: S3 업로드 결과 테스트 * fix: inputstream 변환로직 위치 이동 * fix: 업로드할 s3 path 올바르게 수정 * fix: 사진 url 속에 버킷이름을 cloudfront 도메인으로 수정 * chore: actions 범위 재조정 * feat: API endpoint 변경 * chore: docker image 지우는 작업을 마지막으로 이동 * chore: 다른 브랜치로 이전 커밋 이동하기 위해 제거 * chore: 충돌 해결 및 코드 스타일 변경 * test: S3 이미지 업로드 성공 케이스 추가 * test: multipart form data 문서화 * test: 공모 상태 enum 문서화 * feat: 파일 업로드 크기 제한 100MB에서 20MB로 변경 --------- * feat: 주소검색 기능구현 (#161) * refactor: 네이밍 컨벤션 적용 * build: webview 라이브러리 추가 * feat: 스크립트 실행위한 html파일 추가 * refactor: 인터페이스명 변경에 따른 변경 * feat: 주소검색 다이얼로그 레이아웃 작성 * feat: 주소검색 기능 구현 * style: lint적용 * refactor: 불필요한 코드 제거 * build: Firebase의존성 추가 (#165) * feat: 공모글 작성 API 연결 (#162) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 API 연결 구현 * feat: 공모글 작성 뷰모델 구현 * fix: edit text 데이터바인딩 추가 * chore: 테스트를 위해 MutableLiveData default값 넣어둠 * chore: deadline defualt값 형식에 맞게 수정 * feat: 글작성 화면을 액티비티에서 프래그먼트로 수정 * chore: 테스트목적이었던 주석과 mutable livedata 디폴트값 제거 * refactor: 임시 함수명 수정 * fix: 글작성 프래그먼트가 올라오기 전에 바텀 네비게이션이 사라지는 문제 수정 * feat: 필수 항목이 모두 입력되어야 버튼이 활성화 되는 기능 구현 * feat: 가격, 총원 입력이 잘못되었을 시 토스트를 띄우는 기능 구현 * fix: 버튼 비활성화 시 텍스트 변경 * feat: 앱 아이콘 변경 * feat: 앱 이름 변경(chongdae -> 총대마켓) * feat: 예상 엔빵 가격을 보여주는 기능 구현 * refactor: 상수화 * refactor: 예상 엔빵 가격에 ,가 들어가는 기능 구현, 콜론 뒤 white space 추가 * feat: 공구 할인율을 계산해 주는 기능 구현 * feat: +, - 버튼으로 총원을 조절하는 기능 구현 * fix: 할인율과 엔빵가격 계산 시 0으로 나눠지는 상황을 제거 * fix: 맞춤법 수정 할인률 -> 할인율 * fix: 총원 버튼 크기가 너무 작아서 확대 * fix: 항목간 간격이 좁아서 확대 * refactor: Offering Write의 API service, DataSource, Repository를 Offerings와 합침 * refactor: 디버깅용 코드 삭제 * refactor: 버튼 활성화/비활성화를 selector와 삼항연산자로 구현 * refactor: 바인딩어댑터 대신 뷰모델이 visibility 상태를 갖고 있는 방식으로 변경 * refactor: 바인딩어댑터 대신 xml에서 처리하는 방식으로 변경 * refactor: 총원 디폴트 라이브데이터값 상수화 * refactor: +, - 텍스트뷰 버튼으로 수정 * refactor: textStyle bold대신 fontFamily suit_bold를 쓰는 것으로 수정 * refactor: 변수명 뒤에 Int를 붙이는 것 대신 Value를 붙이는 것으로 수정 * refactor: 글작성 제출 버튼의 아이디를 추가 * refactor: ktFormat * refactor: 토스트를 띄우는 함수 분리 * refactor: 도메인 객체 분리 * refactor: UI모델 적용 * refactor: ktFormat 적용 * feat: 댓글방 디테일 Room을 사용하여 data 저장 (#166) * feat: local database 구현 * feat: entity 구현 * feat: dao 구현 * feat: LocalDataSourceImpl 구현 * feat: entity mapper 구현 * refactor: CommentResponse 에 id 값 추가 * refactor: datasource 이름 변경 및 패키지 변경 * refactor: article -> offering으로 네이밍 변경 * refactor: repository 패키지 변경에 따른 수정 * refactor: datasource 패키지 변경 및 local 과 remote 분리 * refactor: repository Application 클래스를 통한 주입으로 변경 * style: ktlint 적용 * refactor: api service 리네이밍 * refactor: git conflict 해결 * refactor: 함수 이름 컨벤션에 맞도록 변경 (getMeetings -> fetchMeetings) * chore: CI 스크립트 추가 (#173) * chore: ci 스크립트 추가 * chore: ci 스크립트 수정 * fix: og 태그 추출 시 크롤링 이슈 해결 (#174) * feat: 날짜, 시간 선택 기능 구현, 주소검색 기능 연결 (#171) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 모집마감 시간 클릭 시 date time picker를 띄우는 기능 구현 * feat: 날짜, 시간 선택 기능 구현 * feat: 주소 검색 기능 연결 * refactor: 함수명 수정, 함수분리 * refactor: ktFormat 적용 * refactor: string으로 분리, 상수화 * fix: string 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정3 * chore: CI workflow 파일 수정4 * feat: 공모가 정상적으로 게시되었을 시 "공모가 게시되었어요!" 라는 토스트를 띄우고 공모글 작성 프래그먼트를 종료하는 기능 구현 * feat: 토스트가 화면 중앙에 뜨는 문제 수정 * refactor: 사용되지 않는 파일 삭제 * refactor: xml 뷰 id 수정 * refactor: 버튼이 TextView인 문제 수정 * refactor: 사용되지 않는 data binding variable 제거 * refactor: 함수명 수정 * refactor: 다이얼로그, dateTimePickerBinding 전역으로 선언 * refactor: dateTimePicker 클릭 이벤트를 추상화 해 xml에서 처리하도록 변경 * refactor: ktFormat * feat: 상품 URL 이미지 추출 API 연결 (#180) * refactor: 사용하지 않는 파일 제거 * refactor: 가시성 변경 * feat: api service 구현 * feat: datasource 구현 * refactor: repository 네이밍 수정 (offeringsRepository -> offeringRepository) * feat: 사진 업로드 관련 리소스 파일 추가 * feat: repository 및 model 구현 * feat: 이미지 링크를 통한 크롤링 이미지 불러오는 api 연결 및 이미지 삭제 로직 구현 * style: ktlint 적용 * refactor: 이미지 prefix 추가 및 에러 메시지 수정 * refactor: build 오류 수정 * fix: git conflict 해결 * feat: 공모 목록 조회 API에 필터링과 검색 기능 추가 (#169) * feat: 공모 필터 목록 조회 API 구현 * test: 공모 필터 목록 조회 API 테스트 * style: 개행 형식 통일 * feat: 공모 필터 목록 조회 API Specification 도입 준비 * fix: url에 큰따움표 제거 * feat: Specification 도입 * refactor: queryString 구체화 * refactor: 함수명 변경 * feat: 최신순 필터링 적용 * feat: 마감임박순 필터링 적용 * feat: 높은할인률순 필터링 적용 * refactor: 전략 패턴 적용해 여러 갈래의 분기문과 중복되는 코드 처리 * test: 변경된 API 스펙에 맞게 문서화 작업 * refactor: 관련있는 메서드들끼리 모이게 순서 재배치 * refactor: 맞춤법 수정 * style: 개행 제거 --------- * feat: 상태 변경 API 구현 (#175) * feat: 댓글방 상태 변경 및 조회 API 구현 * feat: 공모글 상태 조회 API 구현 * feat: 댓글방 상태 변경 중 수동 확정 기능 구현 * refactor: 상태 변경 관련 메서드명 수정 * refactor: 추상 클래스 메서드 컨벤션 통일 * refactor: errorCode 사용 시 클래스 명시 * refactor: 댓글방 상태 관련 API 엔드포인트 수정 및 패키지 변경 * refactor: 댓글방 상태 변경 API HTTP 메서드 수정 * feat: 공모 모집 자동 확정 시 댓글방 상태 변경 --------- * feat: 로그인 기능 구현 (#177) * feat: password 일방향 암호화 기능 구현 * feat: cookie 생산-소비 기능 구현 * chore: jwt 관련 의존성 추가 * feat: 토큰 생성 기능 구현 * feat: 로그인 API 구현 * test: 로그인 API 테스트 * feat: 회원가입 API 구현 * test: 회원가입 API 테스트 * feat: 닉네임 생성 기능 구현 * test: 닉네임 생성 기능 테스트 * fix: postconstruct 여러 개라 발생한 에러 해결 * feat: 회원가입 응답값에 랜덤생성한 닉네임 추가 * feat: MemberArgumentResolver 구현 * feat: MemberArgumentResolver 일부 적용 * test: 바뀐 스펙에 맞게 변경 * test: TestConfig 설정해 빈충돌 오류 해결 * test: 공모 작성 API로 MemberArgumentResolver 사용 * feat: 토큰 재발급 API 구현 * test: 토큰 재발급 API 테스트 * test: 토큰 재발급 API 에러 테스트 * feat: MemberArgumentResolver commant에 적용 * feat: MemberArgumentResolver offering에 적용 * feat: MemberArgumentResolver participant에 적용 * refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경 * refactor: 클래스명 일관적으로 변경 * refactor: 직관적인 명명으로 enum 네이밍 변경 * refactor: Custom Exception 적용 * refactor: 컨트롤러 메서드에 접근제어자 명시 * fix: 중복된 enum 값 제거 * test: 바뀐 API 스펙에 맞게 변경 --------- * fix: nicknameWordInitializer 설정 오류 해결 (#182) * fix: keyword null일 때 처리 및 docs에서 required 제거 (#184) * fix: keyword null일 때 처리 * test: optional() 붙여서 required 제거 * chore: 브랜치에 상관없이 pr 머지 시 자동으로 관련 이슈 닫는 스크립트 구현 (#187) * fix: og 이미지 태그 크롤링 문제 해결 (#190) * refactor: 댓글방 상태 도메인 설계 변경 (#189) * feat: 공모 목록 API 응답값에 낱개 가격 추가 (#193) * chore: readtimeout 5초로 수정 (#195) * feat: 댓글방 상태 조회 시 상태별 이미지 함께 반환 (#196) * feat: 공모 목록 조회 API연결 (#201) * refactor: Condition 수정에 따른 변경 * refactor: api변경에 따른 리팩토링 * refactor: api변경에 따른 목록 무한 스크롤 기능 리팩토링 * feat: 검색 기능 구현 * feat: 필터링 기능 구현 - 참여 가능은 서버 에러로 추후 추가 예정 * feat: 아이템을 불러온 후 recyclerview의 최상단으로 이동하는 기능 구현 - 검색, 필터링 수행 후 최상단으로 이동 * feat: 필터링 목록 불러오는 api연결 * feat: 마감임박 상태 추가 * refactor: default parameter제거 * style: lint적용 * feat: 토큰 반환 시 cookie가 아닌 body 사용하도록 변경 (#206) * feat: 발급한 토큰을 header가 아닌 body로 반환하도록 수정 * refactor: 사용안하는 클래스와 메서드 제거 * test: 바뀐 API 스펙에 맞게 명세 수정 * feat: 이미지 더미 데이터 수정 및 부정확한 가격 데이터 수정 (#207) * refactor: 공모 글 작성 시 총대 참여자 추가 (#208) * feat: 바텀 네비게이션 고정 기능 구현 (#211) * feat: 데이터에서 5자 이상 제거 (#212) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 (#202) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 * refactor: 도메인 명칭 변경 (낱개가격 -> 원가격) * refactor: 도메인 명칭 변경 (공모 -> 댓글방) * refactor: originPrice로 http client 변경 * feat: 키보드 이외 영역 터치 시 키보드 내려가도록 구현 (#214) * feat: 키보드외 화면 클릭 시 키보드 내려가도록 구현 * refactor: api변경에 다른 dto수정 * feat: 이미지 업로드 및 권한 설정 (#216) * chore: 이미지 권한 추가 * feat: permission manager을 생성하여 권한 체크 및 request * feat: 이미지 추가 버튼을 클릭할 시 권한 설정 연결 * feat: 이미지 피커를 사용하여 uri 전달 구현 * feat: 이미지 파일 업로드 api service 구현 * feat: 이미지 파일 업로드 data source 구현 * feat: 이미지 파일 업로드 repository 구현 * feat: 이미지 파일 martipart로 변환해주는 기능 구현 * feat: 이미지 업로드 관련 뷰 수정 * feat: 이미지 파일 업로드 및 api 연결 구현 * style: ktlint format * fix: git conflict 해결 * refactor: 이미지 scaleType 변경 * refactor: string value 컨벤션 적용 * feat: 토큰 반환 시 body가 아닌 cookie로 반환하도록 원상복구 (#223) * feat: 토큰 재발급 API에서 requestHeader로 refreshToken 받도록 수정 (#227) * feat: 토큰 재발급 API에서 body가 아닌 cookie로 토큰 반환 * feat: 회원가입 API도 body가 아닌 cookie로 토큰 반환 * refactor: service 용 dto 명 컨벤션에 맞춰 수정 * feat: 댓글방 일정 수정 API 구현 (#226) * feat: 댓글방 일정 수정 API 구현 * test: 총대가 아닌 참여자가 공모 일정 정보를 수정할 경우 예외 발생 * feat: 댓글방 상태 조회 시 버튼 텍스트 추가 (#229) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 (#222) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 * refactor: 구현 방식 변경 * style: lint적용 * Feature/217 offering status (#230) * feat: 댓글방 상태 조회 api service 구현 * feat: 댓글방 상태 조회 model 및 dto 구현 * feat: 댓글방 상태 조회 datasource 구현 * feat: 댓글방 상태 조회 repository 구현 * feat: 댓글방 상태 조회 api 연결 구현 * style: ktlint 적용 * feat: 댓글방 상태 변경 (#231) * feat: 댓글방 상태 변경 api service 구현 * feat: 댓글방 상태 변경 data source 구현 * Revert "feat: 댓글방 상태 변경 data source 구현" This reverts commit 052691a8de945c60a60586ee66a05a6a3b264217. * feat: 댓글방 상태 변경 data source 구현 * feat: 댓글방 상태 변경 repository 구현 * feat: 댓글방 상태 변경 api 연결 구현 * style: ktlint 적용 * feature: 카카오 로그인 구현 (#235) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * feat: 공모 참여자 목록 조회 API 구현 (#225) * feat: 공모 참여자 목록 조회 API 구현 * test: 실패 테스트 오류 수정 * style: 띄어쓰기 적용 * refactor: MemberEntity를 받도록 변경 * refactor: isParticipant를 구현하여 가독성 개선 * refactor: 총대를 찾을 수 없는 상황의 예외 추가 * refactor: 참여 검증로직을 서비스로 이동 * refactor: 사용하지 않는 메서드 제거 * refactor: 검증 로직 가장 상단에 위치 * refactor: 총대 추출 로직 수정 --------- * refactor: 마감임박순 필터링 쿼리 조건 수정 (#239) * refactor: 마감임박순 필터링 조건 수정 * refactor: 더미 데이터 시간 수정 * fix: 필터링 오류 수정 (#243) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 (#247) * feat: 공동구매 상태 변경 다이얼로그 구현 (#245) * feat: 공동구매 상태 변경 다이얼로그 view 구현 * feat: 공동구매 상태 변경 다이얼로그 Listener 구현 * feat: 공동구매 상태 변경 다이얼로그 연결 및 상태 변경 로직 수정 * test: 테스트 코드 작성을 위한 기본 세팅 (#255) * feat: CoroutinesTestExtension 구현 * feat: Livedata getOrAwaitValue 구현 * feat: InstantTaskExecutorExtension 구현 * feat: TestFixture 생성 * style: ktlint 적용 * feat: 공모글 목록 화면 UI 개선, 공모글 작성에서 낱개 금액이 엔빵 가격보다 저렴할 시 글 작성 막는 기능 구현 (#246) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 구분선을 각각의 아이템의 하단에 넣고 프래그먼트 뷰의 "채팅" 텍스트 밑에 하나 추가 * fix: 텍스트뷰에 font 적용, 마지막 댓글 시간 텍스트를 조금 왼쪽으로 이동 * fix: 낱개 가격 이름을 eachPrice -> originPrice 수정 * fix: 낱개 가격이 엔빵 가격보다 싸면 토스트를 띄우고 글작성을 막는 기능 구현 * fix: 네이티브앱키 로컬프로퍼티로 이동 * refactor: 함수명 변경 * fix: 카카오 계정으로 로그인 후 액티비티 전환하지 않는 문제 수정 * refactor: 사용되지 않는 클래스 삭제 * refactor: 패키지 수정 * refactor: alsong 로그 수정 * refactor: 변수명 수정 * refactor: Manifest의 네이티브앱 키 숨김 * refactor: 로컬프로퍼티의 데이터 형식 수정 * Update android.yml * refactor: alsong 로그 삭제 * ci 빌드 실패가 manifest때문인지 테스트 * refactor: 매니페스트에 앱 키 넣을 수 있게 하는 gradle 설정 수정 * 매니페스트 수정하고 재테스트 * 매니페스트 수정하고 재테스트 * chore: 그래들 수정 * chore: 그래들 수정2 * chore: 그래들 수정3 * chore: 그래들 수정4 * chore: 카카오 계정으로 로그인하는 기능 제외 * feat: 홈화면 테스트 작성 (#257) * chore: mockk의존성 추가 * test: OfferingViewModel 테스트 작성 * style: lint적용 * refactor: stub를 TestFixture로 이동 * test: 댓글방 테스트 코드 작성 (#258) * refactor: 댓글 보내는 함수명 변경 * refactor: 공구 약속 장소 및 시간 캐시 기능 * test: 테스트를 위한 fake repository 구현 * test: 댓글방 viewmodel test 작성 * feat: 댓글방 ActivityTest 작성 * feat: 댓글방 ActivityTest 작성 * style: ktlint 적용 * refactor: test fixture에서 사용하지 않는 것 삭제 * style: ktlint 적용 * feat: GA 모니터링 환경 구축 및 로깅 전략 적용 (#242) * chore: Firebase Crashlytics 의존성 추가 * feat: Firebase 초기화 * feat: FirebaseManager 구현 * feat: 총대가 공구 진행 상황을 다음 단계로 변경했을 때 event 추가 * feat: 로깅 기능 구현 - 검색 - 필터링 - 공모글 클릭 - 공모 참여 * style: lint적용 * feat: 글 작성 완료 시 event 추가 * feat: 로그인 시 event 추가 --------- * test: 공모글 작성 이미지 테스트 코드 작성 (#260) * refactor: 상수 가시성 변경 * feat: test fixture 구현 * feat: fake repository 이미지 업로드 기능 추가 * test: OfferingWriteViewModelTest 이미지 업로드 test 코드 작성 * feat: 로그인 후 홈화면으로 이동해도 로그인 화면이 종료되지 않는 문제 수정 (#261) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 로그인 후 LoginActivity가 종료되도록 수정 * feat: 공모 상세 화면 테스트 작성 (#264) * feat: OfferingDetailViewModel 테스트 작성 * refactor: 테스트 수정 * style: lint적용 * style: lint적용 * feat: 로깅 코드 삽입 (#266) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 * feature: 로깅 샘플 구현 * refactor: 불필요한 코드 제거 * feat: logging 적용 --------- * fix: 마감 임박 필터링 쿼리 수정 (#267) * chore: logback 설정 진행 (#270) * chore: logback 설정 * fix: multipart 요청 필터링 * chore: logback 설정 변경 * chore: pull request ci/cd 닫기 * fix: 이미지 업로드 API의 responseBody가 두 번 뜨는 오류 해결 (#273) * fix: 이미지 업로드 API 두 번 도는 문제 해결 * test: 이미지 업로드 API의 누락된 response field 추가 * refactor: 홈화면 수정 (#271) * refactor: 할인율 마진 추가 * refactor: 공구상태에 대한 문구 수정 * refactor: 클릭 시 최상단으로 이동하는 버튼 구현 * feat: 공모글 작성 화면 테스트코드 작성 (#274) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: 공모글 작성 테스트 구현 * feat: 댓글방 목록 화면 테스트코드 작성 (#276) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: "댓글방 목록을 확인할 수 있어야 한다" 테스트 작성 * feat: pageSize validation 추가 (#279) * feat: pageSize validation 추가 * feat: magic number 추출 * fix: 공모 상세 화면 오류 수정 (#280) * fix: 총대 여부 확인 로직 수정 * fix: 마감 임박 시 보여주는 버튼 수정 * fix: 공모 작성 후 홈화면으로 돌아왔을 떄 목록이 새로고침 되지 않는 오류 수정 * test: 테스트 코드 수정 * style: lint적용 * feat: 댓글방 목록 화면 자동 업데이트 되지 않는 문제 수정, 회원가입 이후 자동으로 로그인되지 않는 문제 수정 (#282) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 라이플사이클 오너 설정 * fix: 회원가입 후 자동으로 로그인 되도록 수정 * chore: change version name (#291) * feat: 카카오 계정 로그인 기능 구현 시 CI가 실패하는 문제 해결 (#296) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * feat: 로그인 화면 리팩토링 (#298) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * refactor: SimpleCookieJar의 패키지 변경(presentation 레이어에서 data레이어의 source 패키지로 이동) * refactor: data store를 관리하는 클래스를 생성하고 이 클래스를 사용하도록 변경 * refactor: 사용하지 않는 의존성과 주석 제거 * refactor: http status code 추가 * refactor: 함수분리 * refactor: ktFormat 적용 * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentRooms) * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentDetail), 사용되지 않게 된 memberId 제거 * refactor: ktFormat 적용 * test: 테스트코드 수정 * refactor: Preferences -> DataStore 이름 변경 * refactor: 채팅방 UI UX 개선 (#303) * feat: 키보드가 아닌 다른 영역을 클릭하면 키보드 내리는 기능 구현 * feat: 뒤로가는 버튼 기능 추가 * feat: 댓글 입력 maxLines 설정 및 maxLength 설정 * style: ktlint 적용 * 필요 없는 코드 제거 * feat: 댓글방 목록에서 자신이 총대인 댓글방의 UI 개선 (#304) * refactor: 댓글방의 자신이 총대인 댓글방 ui 개선 * fix: Binding 클래스 네이밍 수정 * feat: 가로모드, 다크모드 설정 (#305) * refactor: api변경에 따른 리팩토링 (#310) * feat: 로그인 화면 해상도 대응 (#313) * feat: 이미지 업로드 중일 때 로딩 상태 설정 (#317) * feat: 공모 글 작성 ui state 구현 * feat: 로딩 progressbar 생성 * feat: UI 상태에 따른 토스트 메시지 처리 * refactor: 잘못된 입력에 대한 에러 처리 변경 * refactor: 홈화면 리팩토링 (#324) * refactor: textSize dp로 변경 * refactor: 검색 버튼 크기 변경 - 검색 버튼 패딩 추가 - 검색창 끝에 패딩 추가 * refactor: 엔터키를 통해 검색하도록 수정 * refactor: 필터 단일 선택되도록 수정 * style: lint적용 * feat: 댓글방 새로운 기능 GA 연결 (#328) * feat: 댓글방 참여자 확인 Event 구현 * feat: 댓글방 상태 변경 다이얼로그 취소 Event * feat: 참여자가 공구에서 참여 포기 Event 구현 * style: ktlint 적용 * test: 테스트 데이터 수정 (#330) * feat: Fragment GA 모니터링 수집 (#332) * feat: fragment logScreenView 추적 함수 구현 * feat: 각 fragment에서 화면 감지 GA 설정 * feat: 마이페이지 기본 세팅 및 뷰 변경 (#335) * feat: 공모 참여 취소 기능 구현 (#318) * test: 공모 참여 취소 테스트코드 작성 * feat: 공모 참여 취소 기능 구현 * refactor: 불필요한 쿼리 메서드 제거 * style: 불필요한 개행 제거 * refactor: 모집중인 상태가 아닌 경우 공모 참여를 취소할 수 없도록 변경 * refactor: 공모 참여 취소 응답 상태 코드 변경 * refactor: 에러 메시지 명확한 문구로 변경 * refactor: query parameter를 적용해 어떤 공모의 참여를 취소할 것인지 의도를 명확하게 전달하도록 변경 * refactor: 총대 검증 메서드 네이밍 명확하게 변경 * feat: 댓글방 생성 시점 변경 (#319) * feat: 댓글방 생성 시점 변경 * refactor: 불필요한 도메인 OfferingWithRole 제거 * refactor: 불필요한 도메인 CommentWithRole 제거 * refactor: 댓글의 작성자 확인 메서드 추가 * refactor: 댓글방 목록 조회 dto 생성자 추가 * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 (#322) * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 * refactor: 로그인용 dto 분리 및 공통 dto에 prefix로 auth 추가 * feat: valid 어노테이션 추가 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 (#323) * refactor: 메서드명 구체적으로 변경 * refactor: 변수명 구체적으로 변경 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 * docs: todo 추가 * refactor: 함수명 통일 * feat: 공모자 여부 필드명 변경 * feat: 댓글방 상태 조회 API 확장 (#325) * feat: 댓글방 상태 조회 API 확장 * refactor: 댓글방 관련 로직 댓글 도메인으로 이동 * feat: LoggingFilter에서 던지는 유효하지 않은 요청에 대한 예외 처리 * refactor: 댓글 관련 엔드포인트 수정 * feat: 댓글방 정보 조회 시 조회 권한을 가진 사용자인지 검증 * refactor: 댓글방 상태 확인 로직 도메인으로 이동 * feat: 상태 변경을 시도하는 사용자가 총대인지 검증 * refactor: 댓글 목록 조회 엔드포인트 수정 * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 (#327) * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 * refactor: Response depth 줄이기 및 DTO 생성자 작성 * fix: imminent 필터 버그 해결 (#337) * fix: 커스텀 필터로 인해 h2-console 접속 깨지는 이슈 해결 (#339) * feat: 마이페이지 기능 구현 (#341) * feat: 마이페이지 닉네임 기능 구현 * feat: 로그아웃 로직 구현 * feat: url 연결 로직 구현 * feat: 필요없는 기능 삭제 * style: ktlint 적용 * feat: 공모 테이블에 할인율과 상태 필드 추가 (#342) * refactor: Condition과 Status 이름 변경 * refactor: 사용하지 않는 DTO 제거 * feat: OfferingEntity에 칼럼 추가 * feat: 공모 거래 날짜 필드 이름 변경 (#348) * fix: 상세화면에서 홈화면으로 갔을 때 상태 변경 안되는 오류 수정 (#343) * refactor: 공모상세페이지 Activity -> Fragment로 리팩토링 * fix: 페이지네이션 및 상태변경 미적용 오류 해결 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 주석 제거 및 상수화 * refactor: livedata 자료형 변경 * refactor: progressbar위치 수정 * refactor: lifecycleScope사용 리팩토링 * refactor: adapter에서 전체 아이템이 아닌 특정 아이템만 notify하도록 리팩토링 * refactor: API변경에 따른 대응 (#352) * refactor: api대응 * refactor: api변경에 따른 테스트 수정 * feat: 공모글 작성 화면 ux 개선 (#344) * fix: 각 항목의 설명을 place holder로 이동 * fix: 필수와 선택 항목의 프래그먼트 분리 * feat: 버튼이 항상 보이도록 수정 * fix: 가격과 총원은 숫자만 입력받도록 변경 * fix: 패딩 수정 * fix: ui 수정 * fix: 도메인 변경에 따른 deadline -> tradeDate 수정 * feat: 필수 항목을 모두 입력하면 선택 항목 화면으로 이동하는 기능 구현 * refactor: ktFormat 적용 * refactor: shared viewModel 사용, 미필수 항목을 미필수 입력 화면으로 이동 * refactor: 프래그먼트 이름 변경 * feat: 입력 숫자의 글자수와 라인수 제한 기능 구현 * fix: 총원이 -1이하로 떨어지는 버그 수정, 공동구매 텍스트 띄어쓰기 제거 * fix: 할인율, 엔빵 금액이 유효하지 않을 때는 "-"로 뜨도록 변경 * fix: 공모를 게시하면 필수, 선택 화면 모두 종료되도록 수정 * fix: 날짜 시간 픽커를 날짜만 선택하는 픽커로 변경 * refactor: ktFormat 적용 * refactor: 바인딩어댑터의 파라미터를 nullable하게 수정 * test: 테스트코드 수정 * feat: 낱개 가격의 place holder로 현재 엔빵 금액을 보여주는 기능 구현 * feat: 내용의 최대 글자수와 현재 글자수를 보여주는 기능 구현 * refactor: ktFormat 적용 * refactor: 공모글 작성시 memberId를 보내지 않도록 변경 * fix: 총원 최대 4자리에서 3자리까지만 입력받을 수 있도록 변경 * fix: deadline -> meetingDate 네이밍 수정 * fix: 공모글 작성 후 작성 화면의 입력값이 초기화되지 않는 버그 수정 * refactor: 네이밍 수정(eachPrice -> originPrice) * refactor: 네이밍 수정(individualPrice -> originPrice) * fix: 내용의 현재 글자수 색이 메인컬러가 되지 않는 문제 수정 * refactor: 프래그먼트 종료될 때 바인딩 해제하도록 수정 * refactor: id가 없는 뷰의 id 추가 * refactor: 함수 분리 * fix: 내용 옆의 * 제거 * fix: GA 이벤트 이름 변경(공모글 작성 - 필수 화면에서의 이벤트임을 명시함) * refactor: og 태그 추출 기능 수정 (#349) * refactor: crawler 패키지 이동 * feat: naver api 클라이언트 추가 refactor: 사용하지 않은 기존 og image 크롤러 명칭 변경 * feat: html 크롤링 방식과 naver api 방식을 조합하는 Extractor 구현 * fix: OfferingService ProductImageExtractor 추상화 * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 (#358) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 * test: 테스트코드 수정 * refactor: 공모글 목록 조회 필터링 수정 및 추가 (#356) * refactor: 마감임박순 필터링 이름 마감임박만으로 변경 * refactor: 필터링 쿼리 수정 * feat: "참여가능만" 필터링 기능 구현 * feat: "참여가능만" 필터링 기능 연결 * fix: 쿼리 내 불필요한 파라미터 제거 * refactor: 할인율이 null일 경우 높은할인율 필터링 대상에서 제외 * feat: 참여가능만 필터링 전략 클래스 추가 * feat: 공모 목록 조회 API 응답값 변경 * fix: 높은 할인율 단위 변경 및 last-id 필터링 로직 수정 * style: 주석 제거 --------- * refactor: 할인율 계산 로직 수정 (#359) * refactor: 할인율 계산 로직 수정 * refactor: 소수점 둘째 자리에서 반올림하도록 변경 * test: 할인율 계산 로직 * fix: 할인율 단위 백분율로 수정 --------- * feat: 총 모집 인원 수 최댓값 설정 (#361) * fix: 필터 오류 수정 (#362) * fix: 필터 오류 수정 - '참여가능만'필터 분기처리 제거 * chore: 주석 제거 * feat: API 스펙 변경에 따른 대응 (#364) * feat: 댓글 목록 조회 api 스펙 변경에 따른 대응 * feat: 댓글방 정보 조회 api 스펙 변경에 따른 대응 * feat: 공모 일정 조회 api 스펙 변경에 따른 대응 * feat: 댓글 상태 변경 api 스펙 변경에 따른 대응 * test: api 스펙 변경에 따른 test 코드 변경 * style: ktlint 적용 * feat: remote dto package 분리 * feat: 자동 확정 기능을 위해 스케줄러 적용 (#363) * chore: todo 추가 및 메서드명 변경 * feat: Scheduled 어노테이션 추가 및 Scheduler 분리 * test: ServiceTest 환경 구축 * feat: offeringStatus 변경 로직 추가 * refactor: 수동 확정 로직 추가 및 코드 스타일 수정 * refactor: 자동 확정 로직을 조회에서 Scheduled로 이동 * fix: 마감임박 설정 기준 내일로 변경 --------- * fix: 공모 작성 후 홈화면 돌아올 때 새로 작성한 글이 보이지 않는 오류 수정 (#369) * feat: Access Token, Refresh Token을 data store에 저장하는 기능 구현 (#372) * feat: 앱 재시작 시 토큰을 데이터스토어에서 꺼내 사용하는 기능 구현 * feat: 로그인이 이미 되어있다면 로그인 화면을 건너뛰는 기능 구현 * feat: 로그아웃 기능 구현 * fix: 마이페이지 화면으로 넘어가면 바텀네비게이션이 사라지는 버그 수정 * fix: 데이터스토어에서 토큰이 꺼내지지 않는 버그 수정 data store에서 토큰을 꺼내는 코루틴 비동기 작업이 끝나기 전에 함수를 종료해 버려서 생기는 버그였습니다. * refactor: ktFormat 적용 * refactor: startActivity 함수를 LoginActivity가 동반객체로 갖고 있도록 변경 * refactor: 함수명과 event명 변경 추가로 GA위치가 조금 잘못된 점이 있어서 수정했습니다. * feat: 공모 상세 화면 추가 기능 반영 (#375) * feat: 신고하기 기능 구현 * feat: 물품 링크가 없으면 보여지지 않도록 구현 * refactor: 마감 시간에서 거래 날짜로 리팩토링 * feat: 이미 참여한 공모게시글에서 채팅방으로 이동하는 기능 구현 * fix: 댓글방 목록의 마지막 댓글방이 보이지 않는 문제 수정 (#376) * fix: 리사이클러뷰 레이아웃의 크기가 화면 밖에 벗어나지 않도록 수정 * fix: 리사이클러뷰 레이아웃의 맨 밑에 구분선 하나 추가 아래로 땡겼을 때 구분선이 사라져버리는게 보기 안좋아서 추가했습니다 * refactor: 코트 포맷 적용 (컨트롤 알트 L) * feat: isManualConfirmed 제거 및 도메인 로직 확인 (#377) * refactor: isManualConfirmed 칼럼 삭제 및 관련 로직 분리 * refactor: 더미 데이터 수정 --------- * feat: API 별 권한 확인 로직 추가 (#371) * feat: 권한 확인 로직 추가 * feat: 인증 필터 적용 * refactor: 더미 데이터 칼럼 위치 변경 (#382) * refactor: 홈화면 api필드 추가에 따른 대응 (#381) * refactor: dto필드 추가 * fix: 상태 변경 오류 해결 * fix: 필터 선택 또는 검색상태일 때 공모 작성 후 나오면 목록 안보이는 오류 수정 * refactor: 세부 주소 api에서 받아오도록 변경 * style: lint적용 * fix: API 문서에 접근할 수 없는 현상 해결 (#384) * fix: API 문서에 접근할 수 없는 현상 해결 * style: 신뢰할 수 있는 URL 개행 수정 * feat: 공모 목록에서 동을 보여주는 기능 구현 (#386) * feat: 공모 단건 조회 API 구현 (#388) * feat: 공모 상세 조회 API 엔드포인트 변경 * feat: 공모 단건 조회 API * style: 공모 관련 API 순서 변경 * test: 불필요한 공모글 생성 코드 제거 * test: 공모 단건 조회 서비스 테스트 * refactor: 상태변경 리팩토링 (#389) * refactor: 공모 상세 조회 api변경 대응 * refactor: 공모 상태 변경 리팩토링 * refactor: 리팩토링에 따른 테스트 수정 * chore: 불필요한 로그 제거 * fix: 댓글 입력 후 뒤로가기 시 최근 댓글이 반영되도록 수정 (#397) * chore: JAR 파일에 OAS 파일 누락되는 이슈 해결 및 중복 task 제거 (#391) * chore: 중복되는 task 제거 * chore: cicd 범위 조정 * fix: 참여자 목록 조회 API에서 totalCount 반환하지 않는 이슈 해결 (#400) * feat: 댓글방 참여자 확인 API 연결 (#401) * feat: 참가자 정보를 가져오는 api service 구현 * refactor: 필요없는 코드 삭제 * feat: 참여 관리 datasource 구현 * feat: 참여자 domain 모델 구현 * feat: 참여를 관리하는 repository 구현 * feat: 참여자 목록을 보여주는 recycler view 연결 및 구현 * refactor: 더보기 버튼 수정 * feat: 필요없는 리소스 파일 삭제 및 상태 기본 이미지 변경 * refactor: 약속 장소 및 시간 ui model 을 사용하여 관리 * refactor: 댓글방의 정보를 불러오는 로직 ui model을 사용하여 관리 * refactor: ui model 변환 로직 변경 * feat: 공동구매 참여 인원 확인 기능 구현 * feat: 신고하기 폼 연결 구현 * test: 코드 변경에 따른 테스트 코드 수정 * style: ktlint 적용 * refactor: xml id 추가 * feat: 댓글방 공동구매 나가기 API 연결 (#402) * feat: 공동구매 나가기 기능 api service 구현 * feat: 공동구매 나가기 기능 data source 구현 * feat: 공동구매 나가기 기능 repository 구현 * feat: 공동구매 나가기 기능 연결 * style:ktlint 적용 * fix: /auth/refresh endpoint accessToken 검증 예외 추가 (#407) * refactor: 더미 데이터 정합성 확보 (#406) * refactor: 더미 데이터 정합성 확보 * refactor: 추가된 칼럼 반영 * feat: CallApiHandler 구현 (#403) * feat: CallApiHandler 구현 * refactor: CommentRoomsDataSource 수정 * feat: CommentRemoteDataSourceImpl 에러핸들링을 통해 수정 * feat: 에러 핸들링에 따른 DataSource 리팩토링 - OfferingDetailDataSource - OfferingRemoteDataSource * feat: ParticipantRemoteDataSourceImpl 에러핸들링을 통해 수정 * style: ktlint 적용 * refactor: AuthRemoteDataSource 수정 * feat: Result의 map 과 getOrThrow 함수 생성 * feat: 에러 핸들링에 따른 Repository 리팩토링 - OfferingDetailRepository - OfferingRepository * refactor: Result 변경에 따른 레포지토리 수정 (AuthRepository, CommentRoomsRepository) * feat: 에러 핸들링에 따른 CommentDetailRepository 리팩토링 * feat: 에러 핸들링에 따른 ParticipantRepository 리팩토링 * feat: 에러 핸들링에 따른 viewmodel 리팩토링 - OfferingViewModel - OfferingDetailViewModel * refactor: 에러 핸들링에 따른 LoginViewModel 리팩토링 * refactor: 에러 핸들링에 따른 CommentRoomsViewModel 리팩토링 * refactor: 토큰 리프레쉬 후 다시 함수 호출하도록 추가 * feat: 에러 핸들링에 따른 CommentDetailViewModel 리팩토링 * refactor: 에러 핸들링에 따른 OfferingWriteViewModel 리팩토링 * refactor: 공모 목록 토큰 리프래시 적용 * fix: 잘못된 코드 수정 * refactor: 필요없는 주석 제거 * refactor: 공모 목록 리팩토링 * fix: 리빌드시 쿠키가 제대로 저장되지 않는 현상 수정 * refactor: 필요없는 코드 삭제 및 상수화 추가 * test: 에러핸들링에 따른 FakeAuthRepository, OfferingWriteViewModelTest 수정 * refactor: ktFormat 적용 * test: 코드 변경에 따른 Fake Repository 변경 * test: CommentDetailViewModelTest 코드 수정 * style: ktlint 적용 * refactor: 가독성 개선(에러 로그 함수명 추가, Success가 Error보다 위에 나오도록 수정) * refactor: 불필요한 로그 제거 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 람다 넘겨주는 방식 수정 * style: lint 적용 * test: 테스트코드 수정 --------- * feat: proguard를 사용한 난독화 적용 (#413) * chore: 환경에 따른 yml 파일 분리 (#411) * chore: 환경 별로 yml 파일 분리 * chore: 불필요한 yml 설정 제거 * fix: 공구 상세 페이지 오류 해결 (#417) * fix: 바로가기 클릭되지 않는 오류 수정 * refactor: 주소 표시할 때 최대 2줄까지 그리고 넘어갈 시 말줄임 나오도록 수정 * refactor: 공모 목록, 공모 상세 에러 핸들링 (#418) * refactor: 공모 목록에서 401에러를 제외하고는 에러코드 올 시 빈화면 보여주도록 에러핸들링 수정 * refactor: 필터및 업데이트된 공모 목록 가져오는 로직 에러핸들링 수정 - 400: 토스트 메시지 띄어줌 - 401: refresh - 그외에는 로그로 에러 코드를 보여줌 * refactor: strings네이밍 통일 * refactor: 공모 상세 에러 핸들링 수정 * refactor: strings정리 - offering_detail부분 정리 * feat: 카카오 로그인 중 사용자 정보 확인 로직을 안드로이드에서 백엔드로 이관 (#404) * feat: 카카오 로그인 API 구현 * feat: providerId를 loginId로 수정 * feat: 소셜 로그인 시 랜덤 생성된 비밀번호 사용 * refactor: 불필요한 api 제거 * test: 로그인 로직 변경 * test: MemberFixture 불필요한 함수 제거 및 통일 * refactor: 불필요한 정보 제거 * feat: 카카오 로그인 에러 핸들러 추가 * feat: 민감 정보 로깅에서 제외 --------- * feat: cookie 관련 예외 처리 (#409) * refactor: 더미 데이터 http 추가 (#422) * fix: 더미데이터 정합성 맞추기 (#425) * feat: 로그인 api 변경 반영 (#426) * feat: 카카오 로그인 후 총대마켓 서버로 email을 보내던 방식에서 카카오 access token을 보내는 방식으로 변경 * feat: login과 signup을 하나로 api로 통합된 것 반영 * refactor: ktFormat 적용 * refactor: 테스트코드 수정 * feat: 로깅 시 UUID가 아닌 회원 번호가 기록되도록 변경 (#428) * feat: logging 시 memberId가 나오도록 기능 추가 * feat: logging 시 memberId 및 identifier가 함께 나오도록 변경 * refactor: lombok getter 적용 * feat: Spring Timezone KST로 설정 (#430) * chore: Dockerfile 타임존 변경 (#432) * fix: Offering 목록 조회 시 NPE 해결 (#434) * refactor: 에러 핸들링 리팩토링 (#436) * feat: 리프레시 토큰 만료 시 데이터스토어를 비우고 로그인 화면으로 이동하는 기능 구현 (#438) * feat: 댓글방 에러 헨들링 (#439) * refactor: refresh시 401이 오는 경우에 대한 에러핸들링 추가 (#441) * chore: 버전 업데이트 (#443) * refactor: 외래키 필드 notnull 조건 추가 (#445) * chore: prod CI/CD 구축 (#423) * chore: 환경 별로 yml 파일 분리 * chore: 운영 서버 CI/CD 스크립트 작성 * chore: 운영 환경 내 swagger 문서 제거 * chore: 운영 환경 포트포워딩 명령어 제거 * chore: prod ci/cd 스크립트 트리거 추가 * chore: prod ci/cd 스크립트 트리거 변경 * chore: prod ci/cd 스크립트 트리거 path 구체화 * chore: prod ci/cd 스크립트 docker 실행 명령어 오타 수정 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev 및 prod ci/cd 스크립트 data.sql 실행 비활성화 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev script test --------- * chore: prod 불필요한 트리거 주석 처리 (#447) --------- Co-authored-by: Namyunsuk <84739562+Namyunsuk@users.noreply.github.com> Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: 채현 Co-authored-by: SCY Co-authored-by: alsong <138569524+songpink@users.noreply.github.com> Co-authored-by: masonkimseoul <87306418+masonkimseoul@users.noreply.github.com> Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: masonkimseoul Co-authored-by: fromitive Co-authored-by: Namyunsuk Co-authored-by: songpink * feat: 안드로이드 CD 구축 (#415) * feat: 안드로이드 CD 구축 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * feat: 테스트 * chore: 버전 변경 * chore: 버전 변경 * feat: 비공개테스트 트랙으로 변경 * feat: 비공개테스트 트랙으로 변경 * chore: 버전 변경 * feat: release로 시작하는 branch에서만 CD 작업 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: 채현 * feat: 키보드 이벤트 감지 기능 수정 (#463) * feat: 이미지 로딩 기능 복원 (#465) * fix: 공모 제목 및 가격 줄바꿈, 말줄임 되도록 수정 (#466) * refactor: 홈 화면 UX개선 (#473) * refactor: 필터 텍스트 크기 수정 * refactor: 검색 결과가 없을 시 텍스트를 통해 명시적으로 알려주도록 수정 * refactor: 검색창에 지우기 버튼 만들기 - 클릭 시 검색 텍스트 지워짐 + 전체 공모 보여지는 초기화면보여주도록 * build: SwipeRefreshLayout의존성 추가 * feat: 하단으로 스와이프 시 새로고침 되는 기능 구현 * refactor: 검색창 문구 변경 * refactor: 채팅 UX 개선 (#479) * feat: 나의 채팅 글자 크기 조정 * feat: 채팅 전송 사이즈 변경 * feat: 토큰 리프레시가 제대로 이루어지지 않는 오류 수정 (#480) * fix: Access Token Refresh가 실패하면 이후 로직 처리 * style: ktFormat 적용 * refactor: 필요 없어진 코드 제거 * chore: 필요 없는 주석 제거 * fix: access_token, refresh_token 만료 시 오류 응답 값이 다르게 나오도록 변경 (#478) * test: refreshSecret 키 불일치 수정 * fix: accessToken 만료 시 401 코드를 응답하도록 수정 * chore: 불필요한 파일 삭제 * fix: POSIX 오류 적용 * feat: 공모글 작성 시 이전 날짜 선택 불가하도록 수정, Calendar 방식으로 DatePicker를 변경 (#481) * feat: Date Picker를 달력 형태로 변경 * feat: 현재보다 이전 날짜는 선택할 수 없도록 제한하는 기능 구현 * hotfix: og 크롤링 정상 작동 시 http https 프로토콜 정보 제거 (#458) * feat: 게시글 상세 화면 구현 (#8) * feat: 게시글 상세 화면 레이아웃 작성 * feat: Data layer코드 작성 * refactor: dto패키지 분리, dto에 serialName추가 * refactor: 도메인 모델 수정 - 가변에서 불변으로 변경 - 사용하지 않는 메서드 제거 * refactor: 공통으로 사용되거나 사용될 수 있는 확장함수를 별도의 파일로 분리 * style: lint 적용 * refactor: 메서드명 컨벤션 적용 * refactor: request Dto에 SerialName적용 * refactor: 메서드명 수정 * feat: BottomNavigation 구현 (#16) * chore: jetpack navigation 라이브러리 추가 * feat: 필요한 바텀 네비게이션 리소스 추가 * feat: bottom navigation fragment 추가 * feat: bottom navigation graph 구현 * refactor: 컨벤션에 맞게 id 수정 * feat: 홈화면, 마이페이지 화면 레이아웃 작성 (#19) * refactor: FragmentContainer width 속성 수정 * feat: 홈 화면 레이아웃 작성 * feat: 마이페이지 화면 레이아웃 작성 * fix: 플로팅 버튼이 홈에서만 보이도록 수정 * refactor: 리소스 네이밍 컨벤션에 맞게 수정 * feat: 댓글방 목록 구현 (#26) * feat: 댓글방 목록 UI 구현 * fix: 구분선을 ImageView에서 View로 변경 * feat: 댓글방 목록 도메인 모델 구현 * feat: 댓글방 어답터 구현 * feat: "채팅" string 추가 * refactor: 불필요한 코드 제거 * fix: xmls 중복 속성 제거 * refactor: 댓글방 클래스들을 comment 패키지로 분리 * refactor: 컬러와 폰트 사이즈를 values 파일로 분리 * feat: 댓글방 디테일 화면 구현 (#32) * feat: font 설정 * feat: vector 이미지 추가 * feat: 채팅 아이템 뷰 구현 * refactor: 컨벤션에 맞게 네이밍 수정 * feat: 댓글 입력 edit text 구현 * feat: 공모 상세 페이지 API 연결 (#46) * build: 불필요한 의존성 제거, properties관련 코드 작성 * refactor: base_url코드상에서 제거 * feat: api수정에 따른 필드 변경 및 네이밍 반영 * refactor: 네이밍 변경 * refactor: OfferingDetail의 변경, mapper변경 * refactor: service분리 * refactor: DataSource, Repository분리 * refactor: API변경에 따른 리팩토링 * feat: 공모 상세 조회 기능 구현 * refactor: 참여하기 api변경에 따른 data, domain 코드 수정 * feat: 공모 상세 페이지 참여하기 기능 구현 * feat: 공모 상세 화면에서 이미지를 불러올 수 없을 시 기본이미지를 보여주는 기능 구현 * feat: 게시물 상세 화면 폰트 적용 * style: lint적용 * refactor: 액티비티 destroy시 binding해제하도록 코드 추가 * refactor: glide옵션 변경 - 에러 발생 시 보여줄 이미지 - url이 null일 시 보여줄 이미지 * refactor: viewModel에 custom getter추가 * fix: 내용이 짧을 시 뒷 배경이 회색으로 보이는 버그 수정 * fix: 참여하기 버튼을 눌렀을 시 텍스트가 바뀌지 않는 버그 수정 * chore: 안드로이드 CI 파일 작성 (#63) * chore: build CI 작업을 위한 manifest 파일 수정 (#65) * chore: 알람 권한 추가 * chore: local properties 속성 추가 * chore: local properties null 체크 로직 추가 * chore: buildConfigField null 체크 * style: lint 적용 * chore: secret 값 설정 * fix: secret 값 오류 수정 * fix: 문법 오류 수정 * chore: 경로 수정 * chore: 문법 수정 * style: lint 적용 * feat 댓글방 접히는 공지 뷰 구현 (#72) * chore: manifest에 CommentDetailActivity 추가 * feat: BindingAdatper을 사용하여 접힐 때 애니메이션 적용 및 픽셀 변환 * feat: viewmodel 구현 및 click 마다 접히고 펴지는 로직 구현 * style: ktlint 적용 * refactor: binding adpater을 사용하여 가시성 변경 * feat: 홈화면 API 연결 (#74) * refactor: API변경에 따른 data, domain 코드 변경 * feat: 공모 목록 기능 구현 * refactor: 함수 분리 * style: lint적용 * style: font 적용 * feat: 댓글방 목록 API 연결 (#82) * feat: bottom navigation fragment 추가 * feat: vector 이미지 추가 * feat: 댓글방이 없으면 "채팅 목록이 없어요" 라는 텍스트뷰와 이미지뷰를 띄우는 기능 구현 * feat: 댓글방 띄우는 기능 구현 * test: 댓글방 UI 테스트 작성 * refactor: 테스트 클래스명 수정 * refactor: 줄바꿈 수정 * feat: 댓글방 API 서비스 구현 * refactor: API 명세에 따라 도메인 모델 수정 * feat: API 연결 * refactor: API명세에 따라 데이터바인딩 변수명 수정 * feat: 댓글방 목록 API 연결 * refactor: ktlint Format 적용 * refactor: 메모리 누수 방지를 위해 fragment가 destroy 될 때 _binding을 null로 설정 * refactor: 어답터를 방어적복사 하지 않아도 되어서 수정 * refactor: 채팅방이 없다는 이미지뷰를 띄워주는 방식 수정(바인딩 어댑터 수정) * refactor: 함수분리 * refactor: ktFormat 적용 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> * feat: 댓글방 접히는 공지 API 연결 (#85) * feat: 미팅 일정 API 연결을 위한 data layer 구현 * feat: 미팅 일정 API 연결을 위한 domain layer 구현 * feat: 미팅 일정 API 연결을 위한 presentation layer 구현 * style: ktlint 적용 * feat: 공동 구매 제목 databinding 적용 * refactor: 변수명 수정 * fix: 펼치기 접기 버튼 로직 반대로 수정 * style: ktlint 적용 * feat: 공모 상세 페이지 기능 추가 (#94) * chore: 마이페이지 닉네임 임시로 지정 * feat: 바로가기 기능 구현 * feat: 참여버튼 클릭 시 댓글방으로 가도록 기능 구현 * feat: 신고하기 이미지 추가 * style: lint적용 * refactor: 불러오는 공모 페이지 사이즈 변경 * feat: 댓글방 댓글 작성 api 연결 (#95) * chore: windowSoftInputMode 추가 * feat: post comment api service 구현 * feat: post comment DataSource 구현 * feat: post comment Repository 구현 * feat: post comment Presentation 구현 * feat: 댓글방 입장 기능, 본인이 총대인 방은 다르게 보이는 기능 구현 (#99) * feat: 댓글방의 마지막 댓글 시간을 띄우는 기능 구현 * feat: 자신이 총대인 댓글방을 표시하는 기능 구현 * feat: 댓글방 목록을 클릭해 댓글방 상세로 이동하는 기능 구현 * test: UI테스트 수정 * refactor: 클릭시 id 뿐만 아니라 title도 받아오는 방식으로 수정 * refactor: 오전/오후와 시간을 텍스트뷰에 띄우는 바인딩 어댑터를 DateTimeFormatter의 기능을 사용하는 것으로 수정 * refactor: memberId를 local.properties의 token을 가져다 쓰는 것으로 변경(임시 조치) * refactor: 댓글방 목록의 시간을 띄우는 바인딩 어댑터의 속성명을 수정함 * refactor: 데이터바인딩 variable 변수명을 구체적으로 수정, 일관성을 위해 앞에 `on` 붙임 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정(빠트린것 수정함) * feat: 홈 화면 무한 스크롤 기능 구현 (#109) * build: pagination라이브러리 추가 * feat: 홈 화면 무한 스크롤 기능 구현 * fix: 마지막 댓글 response를 nullable하게 수정 (#115) * fix: 마지막 댓글 response를 nullable하게 수정 * refactor: ktFormat 적용 * feat: 댓글방 댓글 조회 api 연결 (#116) * feat: dto 및 mapper 구현 * feat: 댓글방 목록 service 구현 * feat: 댓글방 목록 data source 구현 * feat: 댓글방 목록 repository 및 model 구현 * feat: 댓글방 목록 view type을 활용한 recyclerview 구현 및 데이터 바인딩 * feat: polling 기능 구현 * feat: 댓글 스크롤 구현 (새로운 댓글이 생길시 스크롤 아래로) * feat: 총대와 다른 참가자 이미지 리소스 파일 * feat: 댓글방 디테일 공동 구매 상태별 관리 (#117) * feat: 공동구매 상태 관리 리소스 파일 * feat: 공동구매 상태를 관리하는 enum class 구현 * feat: 데이터바인딩을 사용하여 공동 구매 상태 뷰 업데이트 구현 * style: ktlint 적용 * feat: 공동구매 상태 관리 리소스 파일 추가 * refactor: 네이밍 수정 (#123) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 (#125) * refactor: 뷰모델 팩토리 방식 변경 (#130) * refactor: 뷰모델 팩토리를 뷰모델의 동반객체로 이동 * style: lint적용 * refactor: Service분리 (#132) * refactor: service분리 * refactor: 패키지명 변경 * style: lint적용 * feat: 공모글 작성 UI 구현 (#134) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 뷰 구현 * fix: 뷰 수정사항 반영 * fix: @+id로 참조하는 부분을 수정 * fix: drawable의 네이밍에 where을 추가 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 (#136) * feat: 참여자 목록 drawer에 필요한 리소스 파일 추가 * refactor: 채팅 text gravity 수정 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 * style: ktlint 적용 * refactor: drawer early return 하는 방식으로 변경 * refactor: ivMore -> ivMoreOptions으로 네이밍 변경 * feat: 공구 참여자 item view 및 댓글방 view 사용자 친화적으로 수정 * feat: 홈화면(공모목록) UI 추가 구현 및 상태 변경 대응 (#142) * feat: 공모의 상태 변경이 반영되도록 기능 구현 * feat: 공모 목록 ui변경 * feat: 필터 ui추가 * feat: API변경에 따른 DTO수정 * style: lint적용 * feat: resource추가 * refactor: ui위치 수정 * chore: 불필요한 괄호 제거 * refactor: item 수직 정렬 * feat: 주소검색 기능구현 (#161) * refactor: 네이밍 컨벤션 적용 * build: webview 라이브러리 추가 * feat: 스크립트 실행위한 html파일 추가 * refactor: 인터페이스명 변경에 따른 변경 * feat: 주소검색 다이얼로그 레이아웃 작성 * feat: 주소검색 기능 구현 * style: lint적용 * refactor: 불필요한 코드 제거 * build: Firebase의존성 추가 (#165) * feat: 공모글 작성 API 연결 (#162) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 API 연결 구현 * feat: 공모글 작성 뷰모델 구현 * fix: edit text 데이터바인딩 추가 * chore: 테스트를 위해 MutableLiveData default값 넣어둠 * chore: deadline defualt값 형식에 맞게 수정 * feat: 글작성 화면을 액티비티에서 프래그먼트로 수정 * chore: 테스트목적이었던 주석과 mutable livedata 디폴트값 제거 * refactor: 임시 함수명 수정 * fix: 글작성 프래그먼트가 올라오기 전에 바텀 네비게이션이 사라지는 문제 수정 * feat: 필수 항목이 모두 입력되어야 버튼이 활성화 되는 기능 구현 * feat: 가격, 총원 입력이 잘못되었을 시 토스트를 띄우는 기능 구현 * fix: 버튼 비활성화 시 텍스트 변경 * feat: 앱 아이콘 변경 * feat: 앱 이름 변경(chongdae -> 총대마켓) * feat: 예상 엔빵 가격을 보여주는 기능 구현 * refactor: 상수화 * refactor: 예상 엔빵 가격에 ,가 들어가는 기능 구현, 콜론 뒤 white space 추가 * feat: 공구 할인율을 계산해 주는 기능 구현 * feat: +, - 버튼으로 총원을 조절하는 기능 구현 * fix: 할인율과 엔빵가격 계산 시 0으로 나눠지는 상황을 제거 * fix: 맞춤법 수정 할인률 -> 할인율 * fix: 총원 버튼 크기가 너무 작아서 확대 * fix: 항목간 간격이 좁아서 확대 * refactor: Offering Write의 API service, DataSource, Repository를 Offerings와 합침 * refactor: 디버깅용 코드 삭제 * refactor: 버튼 활성화/비활성화를 selector와 삼항연산자로 구현 * refactor: 바인딩어댑터 대신 뷰모델이 visibility 상태를 갖고 있는 방식으로 변경 * refactor: 바인딩어댑터 대신 xml에서 처리하는 방식으로 변경 * refactor: 총원 디폴트 라이브데이터값 상수화 * refactor: +, - 텍스트뷰 버튼으로 수정 * refactor: textStyle bold대신 fontFamily suit_bold를 쓰는 것으로 수정 * refactor: 변수명 뒤에 Int를 붙이는 것 대신 Value를 붙이는 것으로 수정 * refactor: 글작성 제출 버튼의 아이디를 추가 * refactor: ktFormat * refactor: 토스트를 띄우는 함수 분리 * refactor: 도메인 객체 분리 * refactor: UI모델 적용 * refactor: ktFormat 적용 * feat: 댓글방 디테일 Room을 사용하여 data 저장 (#166) * feat: local database 구현 * feat: entity 구현 * feat: dao 구현 * feat: LocalDataSourceImpl 구현 * feat: entity mapper 구현 * refactor: CommentResponse 에 id 값 추가 * refactor: datasource 이름 변경 및 패키지 변경 * refactor: article -> offering으로 네이밍 변경 * refactor: repository 패키지 변경에 따른 수정 * refactor: datasource 패키지 변경 및 local 과 remote 분리 * refactor: repository Application 클래스를 통한 주입으로 변경 * style: ktlint 적용 * refactor: api service 리네이밍 * refactor: git conflict 해결 * refactor: 함수 이름 컨벤션에 맞도록 변경 (getMeetings -> fetchMeetings) * chore: CI 스크립트 추가 (#173) * chore: ci 스크립트 추가 * chore: ci 스크립트 수정 * feat: 날짜, 시간 선택 기능 구현, 주소검색 기능 연결 (#171) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 모집마감 시간 클릭 시 date time picker를 띄우는 기능 구현 * feat: 날짜, 시간 선택 기능 구현 * feat: 주소 검색 기능 연결 * refactor: 함수명 수정, 함수분리 * refactor: ktFormat 적용 * refactor: string으로 분리, 상수화 * fix: string 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정3 * chore: CI workflow 파일 수정4 * feat: 공모가 정상적으로 게시되었을 시 "공모가 게시되었어요!" 라는 토스트를 띄우고 공모글 작성 프래그먼트를 종료하는 기능 구현 * feat: 토스트가 화면 중앙에 뜨는 문제 수정 * refactor: 사용되지 않는 파일 삭제 * refactor: xml 뷰 id 수정 * refactor: 버튼이 TextView인 문제 수정 * refactor: 사용되지 않는 data binding variable 제거 * refactor: 함수명 수정 * refactor: 다이얼로그, dateTimePickerBinding 전역으로 선언 * refactor: dateTimePicker 클릭 이벤트를 추상화 해 xml에서 처리하도록 변경 * refactor: ktFormat * feat: 상품 URL 이미지 추출 API 연결 (#180) * refactor: 사용하지 않는 파일 제거 * refactor: 가시성 변경 * feat: api service 구현 * feat: datasource 구현 * refactor: repository 네이밍 수정 (offeringsRepository -> offeringRepository) * feat: 사진 업로드 관련 리소스 파일 추가 * feat: repository 및 model 구현 * feat: 이미지 링크를 통한 크롤링 이미지 불러오는 api 연결 및 이미지 삭제 로직 구현 * style: ktlint 적용 * refactor: 이미지 prefix 추가 및 에러 메시지 수정 * refactor: build 오류 수정 * fix: git conflict 해결 * feat: 공모 목록 조회 API연결 (#201) * refactor: Condition 수정에 따른 변경 * refactor: api변경에 따른 리팩토링 * refactor: api변경에 따른 목록 무한 스크롤 기능 리팩토링 * feat: 검색 기능 구현 * feat: 필터링 기능 구현 - 참여 가능은 서버 에러로 추후 추가 예정 * feat: 아이템을 불러온 후 recyclerview의 최상단으로 이동하는 기능 구현 - 검색, 필터링 수행 후 최상단으로 이동 * feat: 필터링 목록 불러오는 api연결 * feat: 마감임박 상태 추가 * refactor: default parameter제거 * style: lint적용 * feat: 바텀 네비게이션 고정 기능 구현 (#211) * feat: 키보드 이외 영역 터치 시 키보드 내려가도록 구현 (#214) * feat: 키보드외 화면 클릭 시 키보드 내려가도록 구현 * refactor: api변경에 다른 dto수정 * feat: 이미지 업로드 및 권한 설정 (#216) * chore: 이미지 권한 추가 * feat: permission manager을 생성하여 권한 체크 및 request * feat: 이미지 추가 버튼을 클릭할 시 권한 설정 연결 * feat: 이미지 피커를 사용하여 uri 전달 구현 * feat: 이미지 파일 업로드 api service 구현 * feat: 이미지 파일 업로드 data source 구현 * feat: 이미지 파일 업로드 repository 구현 * feat: 이미지 파일 martipart로 변환해주는 기능 구현 * feat: 이미지 업로드 관련 뷰 수정 * feat: 이미지 파일 업로드 및 api 연결 구현 * style: ktlint format * fix: git conflict 해결 * refactor: 이미지 scaleType 변경 * refactor: string value 컨벤션 적용 * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 (#222) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 * refactor: 구현 방식 변경 * style: lint적용 * Feature/217 offering status (#230) * feat: 댓글방 상태 조회 api service 구현 * feat: 댓글방 상태 조회 model 및 dto 구현 * feat: 댓글방 상태 조회 datasource 구현 * feat: 댓글방 상태 조회 repository 구현 * feat: 댓글방 상태 조회 api 연결 구현 * style: ktlint 적용 * feat: 댓글방 상태 변경 (#231) * feat: 댓글방 상태 변경 api service 구현 * feat: 댓글방 상태 변경 data source 구현 * Revert "feat: 댓글방 상태 변경 data source 구현" This reverts commit 052691a8de945c60a60586ee66a05a6a3b264217. * feat: 댓글방 상태 변경 data source 구현 * feat: 댓글방 상태 변경 repository 구현 * feat: 댓글방 상태 변경 api 연결 구현 * style: ktlint 적용 * feature: 카카오 로그인 구현 (#235) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 필터링 오류 수정 (#243) * feat: 공동구매 상태 변경 다이얼로그 구현 (#245) * feat: 공동구매 상태 변경 다이얼로그 view 구현 * feat: 공동구매 상태 변경 다이얼로그 Listener 구현 * feat: 공동구매 상태 변경 다이얼로그 연결 및 상태 변경 로직 수정 * test: 테스트 코드 작성을 위한 기본 세팅 (#255) * feat: CoroutinesTestExtension 구현 * feat: Livedata getOrAwaitValue 구현 * feat: InstantTaskExecutorExtension 구현 * feat: TestFixture 생성 * style: ktlint 적용 * feat: 공모글 목록 화면 UI 개선, 공모글 작성에서 낱개 금액이 엔빵 가격보다 저렴할 시 글 작성 막는 기능 구현 (#246) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 구분선을 각각의 아이템의 하단에 넣고 프래그먼트 뷰의 "채팅" 텍스트 밑에 하나 추가 * fix: 텍스트뷰에 font 적용, 마지막 댓글 시간 텍스트를 조금 왼쪽으로 이동 * fix: 낱개 가격 이름을 eachPrice -> originPrice 수정 * fix: 낱개 가격이 엔빵 가격보다 싸면 토스트를 띄우고 글작성을 막는 기능 구현 * fix: 네이티브앱키 로컬프로퍼티로 이동 * refactor: 함수명 변경 * fix: 카카오 계정으로 로그인 후 액티비티 전환하지 않는 문제 수정 * refactor: 사용되지 않는 클래스 삭제 * refactor: 패키지 수정 * refactor: alsong 로그 수정 * refactor: 변수명 수정 * refactor: Manifest의 네이티브앱 키 숨김 * refactor: 로컬프로퍼티의 데이터 형식 수정 * Update android.yml * refactor: alsong 로그 삭제 * ci 빌드 실패가 manifest때문인지 테스트 * refactor: 매니페스트에 앱 키 넣을 수 있게 하는 gradle 설정 수정 * 매니페스트 수정하고 재테스트 * 매니페스트 수정하고 재테스트 * chore: 그래들 수정 * chore: 그래들 수정2 * chore: 그래들 수정3 * chore: 그래들 수정4 * chore: 카카오 계정으로 로그인하는 기능 제외 * feat: 홈화면 테스트 작성 (#257) * chore: mockk의존성 추가 * test: OfferingViewModel 테스트 작성 * style: lint적용 * refactor: stub를 TestFixture로 이동 * test: 댓글방 테스트 코드 작성 (#258) * refactor: 댓글 보내는 함수명 변경 * refactor: 공구 약속 장소 및 시간 캐시 기능 * test: 테스트를 위한 fake repository 구현 * test: 댓글방 viewmodel test 작성 * feat: 댓글방 ActivityTest 작성 * feat: 댓글방 ActivityTest 작성 * style: ktlint 적용 * refactor: test fixture에서 사용하지 않는 것 삭제 * style: ktlint 적용 * feat: GA 모니터링 환경 구축 및 로깅 전략 적용 (#242) * chore: Firebase Crashlytics 의존성 추가 * feat: Firebase 초기화 * feat: FirebaseManager 구현 * feat: 총대가 공구 진행 상황을 다음 단계로 변경했을 때 event 추가 * feat: 로깅 기능 구현 - 검색 - 필터링 - 공모글 클릭 - 공모 참여 * style: lint적용 * feat: 글 작성 완료 시 event 추가 * feat: 로그인 시 event 추가 --------- Co-authored-by: Namyunsuk Co-authored-by: songpink * test: 공모글 작성 이미지 테스트 코드 작성 (#260) * refactor: 상수 가시성 변경 * feat: test fixture 구현 * feat: fake repository 이미지 업로드 기능 추가 * test: OfferingWriteViewModelTest 이미지 업로드 test 코드 작성 * feat: 로그인 후 홈화면으로 이동해도 로그인 화면이 종료되지 않는 문제 수정 (#261) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 로그인 후 LoginActivity가 종료되도록 수정 * feat: 공모 상세 화면 테스트 작성 (#264) * feat: OfferingDetailViewModel 테스트 작성 * refactor: 테스트 수정 * style: lint적용 * style: lint적용 * refactor: 홈화면 수정 (#271) * refactor: 할인율 마진 추가 * refactor: 공구상태에 대한 문구 수정 * refactor: 클릭 시 최상단으로 이동하는 버튼 구현 * feat: 공모글 작성 화면 테스트코드 작성 (#274) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: 공모글 작성 테스트 구현 * feat: 댓글방 목록 화면 테스트코드 작성 (#276) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: "댓글방 목록을 확인할 수 있어야 한다" 테스트 작성 * fix: 공모 상세 화면 오류 수정 (#280) * fix: 총대 여부 확인 로직 수정 * fix: 마감 임박 시 보여주는 버튼 수정 * fix: 공모 작성 후 홈화면으로 돌아왔을 떄 목록이 새로고침 되지 않는 오류 수정 * test: 테스트 코드 수정 * style: lint적용 * feat: 댓글방 목록 화면 자동 업데이트 되지 않는 문제 수정, 회원가입 이후 자동으로 로그인되지 않는 문제 수정 (#282) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 라이플사이클 오너 설정 * fix: 회원가입 후 자동으로 로그인 되도록 수정 * chore: change version name (#291) * feat: 카카오 계정 로그인 기능 구현 시 CI가 실패하는 문제 해결 (#296) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * feat: 로그인 화면 리팩토링 (#298) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * refactor: SimpleCookieJar의 패키지 변경(presentation 레이어에서 data레이어의 source 패키지로 이동) * refactor: data store를 관리하는 클래스를 생성하고 이 클래스를 사용하도록 변경 * refactor: 사용하지 않는 의존성과 주석 제거 * refactor: http status code 추가 * refactor: 함수분리 * refactor: ktFormat 적용 * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentRooms) * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentDetail), 사용되지 않게 된 memberId 제거 * refactor: ktFormat 적용 * test: 테스트코드 수정 * refactor: Preferences -> DataStore 이름 변경 * refactor: 채팅방 UI UX 개선 (#303) * feat: 키보드가 아닌 다른 영역을 클릭하면 키보드 내리는 기능 구현 * feat: 뒤로가는 버튼 기능 추가 * feat: 댓글 입력 maxLines 설정 및 maxLength 설정 * style: ktlint 적용 * 필요 없는 코드 제거 * feat: 댓글방 목록에서 자신이 총대인 댓글방의 UI 개선 (#304) * refactor: 댓글방의 자신이 총대인 댓글방 ui 개선 * fix: Binding 클래스 네이밍 수정 * feat: 가로모드, 다크모드 설정 (#305) * refactor: api변경에 따른 리팩토링 (#310) * feat: 로그인 화면 해상도 대응 (#313) * feat: 이미지 업로드 중일 때 로딩 상태 설정 (#317) * feat: 공모 글 작성 ui state 구현 * feat: 로딩 progressbar 생성 * feat: UI 상태에 따른 토스트 메시지 처리 * refactor: 잘못된 입력에 대한 에러 처리 변경 * refactor: 홈화면 리팩토링 (#324) * refactor: textSize dp로 변경 * refactor: 검색 버튼 크기 변경 - 검색 버튼 패딩 추가 - 검색창 끝에 패딩 추가 * refactor: 엔터키를 통해 검색하도록 수정 * refactor: 필터 단일 선택되도록 수정 * style: lint적용 * feat: 댓글방 새로운 기능 GA 연결 (#328) * feat: 댓글방 참여자 확인 Event 구현 * feat: 댓글방 상태 변경 다이얼로그 취소 Event * feat: 참여자가 공구에서 참여 포기 Event 구현 * style: ktlint 적용 * feat: Fragment GA 모니터링 수집 (#332) * feat: fragment logScreenView 추적 함수 구현 * feat: 각 fragment에서 화면 감지 GA 설정 * feat: 마이페이지 기본 세팅 및 뷰 변경 (#335) * feat: 마이페이지 기능 구현 (#341) * feat: 마이페이지 닉네임 기능 구현 * feat: 로그아웃 로직 구현 * feat: url 연결 로직 구현 * feat: 필요없는 기능 삭제 * style: ktlint 적용 * fix: 상세화면에서 홈화면으로 갔을 때 상태 변경 안되는 오류 수정 (#343) * refactor: 공모상세페이지 Activity -> Fragment로 리팩토링 * fix: 페이지네이션 및 상태변경 미적용 오류 해결 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 주석 제거 및 상수화 * refactor: livedata 자료형 변경 * refactor: progressbar위치 수정 * refactor: lifecycleScope사용 리팩토링 * refactor: adapter에서 전체 아이템이 아닌 특정 아이템만 notify하도록 리팩토링 * refactor: API변경에 따른 대응 (#352) * refactor: api대응 * refactor: api변경에 따른 테스트 수정 * feat: 공모글 작성 화면 ux 개선 (#344) * fix: 각 항목의 설명을 place holder로 이동 * fix: 필수와 선택 항목의 프래그먼트 분리 * feat: 버튼이 항상 보이도록 수정 * fix: 가격과 총원은 숫자만 입력받도록 변경 * fix: 패딩 수정 * fix: ui 수정 * fix: 도메인 변경에 따른 deadline -> tradeDate 수정 * feat: 필수 항목을 모두 입력하면 선택 항목 화면으로 이동하는 기능 구현 * refactor: ktFormat 적용 * refactor: shared viewModel 사용, 미필수 항목을 미필수 입력 화면으로 이동 * refactor: 프래그먼트 이름 변경 * feat: 입력 숫자의 글자수와 라인수 제한 기능 구현 * fix: 총원이 -1이하로 떨어지는 버그 수정, 공동구매 텍스트 띄어쓰기 제거 * fix: 할인율, 엔빵 금액이 유효하지 않을 때는 "-"로 뜨도록 변경 * fix: 공모를 게시하면 필수, 선택 화면 모두 종료되도록 수정 * fix: 날짜 시간 픽커를 날짜만 선택하는 픽커로 변경 * refactor: ktFormat 적용 * refactor: 바인딩어댑터의 파라미터를 nullable하게 수정 * test: 테스트코드 수정 * feat: 낱개 가격의 place holder로 현재 엔빵 금액을 보여주는 기능 구현 * feat: 내용의 최대 글자수와 현재 글자수를 보여주는 기능 구현 * refactor: ktFormat 적용 * refactor: 공모글 작성시 memberId를 보내지 않도록 변경 * fix: 총원 최대 4자리에서 3자리까지만 입력받을 수 있도록 변경 * fix: deadline -> meetingDate 네이밍 수정 * fix: 공모글 작성 후 작성 화면의 입력값이 초기화되지 않는 버그 수정 * refactor: 네이밍 수정(eachPrice -> originPrice) * refactor: 네이밍 수정(individualPrice -> originPrice) * fix: 내용의 현재 글자수 색이 메인컬러가 되지 않는 문제 수정 * refactor: 프래그먼트 종료될 때 바인딩 해제하도록 수정 * refactor: id가 없는 뷰의 id 추가 * refactor: 함수 분리 * fix: 내용 옆의 * 제거 * fix: GA 이벤트 이름 변경(공모글 작성 - 필수 화면에서의 이벤트임을 명시함) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 (#358) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 * test: 테스트코드 수정 * fix: 필터 오류 수정 (#362) * fix: 필터 오류 수정 - '참여가능만'필터 분기처리 제거 * chore: 주석 제거 * feat: API 스펙 변경에 따른 대응 (#364) * feat: 댓글 목록 조회 api 스펙 변경에 따른 대응 * feat: 댓글방 정보 조회 api 스펙 변경에 따른 대응 * feat: 공모 일정 조회 api 스펙 변경에 따른 대응 * feat: 댓글 상태 변경 api 스펙 변경에 따른 대응 * test: api 스펙 변경에 따른 test 코드 변경 * style: ktlint 적용 * feat: remote dto package 분리 * fix: 공모 작성 후 홈화면 돌아올 때 새로 작성한 글이 보이지 않는 오류 수정 (#369) * feat: Access Token, Refresh Token을 data store에 저장하는 기능 구현 (#372) * feat: 앱 재시작 시 토큰을 데이터스토어에서 꺼내 사용하는 기능 구현 * feat: 로그인이 이미 되어있다면 로그인 화면을 건너뛰는 기능 구현 * feat: 로그아웃 기능 구현 * fix: 마이페이지 화면으로 넘어가면 바텀네비게이션이 사라지는 버그 수정 * fix: 데이터스토어에서 토큰이 꺼내지지 않는 버그 수정 data store에서 토큰을 꺼내는 코루틴 비동기 작업이 끝나기 전에 함수를 종료해 버려서 생기는 버그였습니다. * refactor: ktFormat 적용 * refactor: startActivity 함수를 LoginActivity가 동반객체로 갖고 있도록 변경 * refactor: 함수명과 event명 변경 추가로 GA위치가 조금 잘못된 점이 있어서 수정했습니다. * feat: 공모 상세 화면 추가 기능 반영 (#375) * feat: 신고하기 기능 구현 * feat: 물품 링크가 없으면 보여지지 않도록 구현 * refactor: 마감 시간에서 거래 날짜로 리팩토링 * feat: 이미 참여한 공모게시글에서 채팅방으로 이동하는 기능 구현 * fix: 댓글방 목록의 마지막 댓글방이 보이지 않는 문제 수정 (#376) * fix: 리사이클러뷰 레이아웃의 크기가 화면 밖에 벗어나지 않도록 수정 * fix: 리사이클러뷰 레이아웃의 맨 밑에 구분선 하나 추가 아래로 땡겼을 때 구분선이 사라져버리는게 보기 안좋아서 추가했습니다 * refactor: 코트 포맷 적용 (컨트롤 알트 L) * refactor: 홈화면 api필드 추가에 따른 대응 (#381) * refactor: dto필드 추가 * fix: 상태 변경 오류 해결 * fix: 필터 선택 또는 검색상태일 때 공모 작성 후 나오면 목록 안보이는 오류 수정 * refactor: 세부 주소 api에서 받아오도록 변경 * style: lint적용 * feat: 공모 목록에서 동을 보여주는 기능 구현 (#386) * refactor: 상태변경 리팩토링 (#389) * refactor: 공모 상세 조회 api변경 대응 * refactor: 공모 상태 변경 리팩토링 * refactor: 리팩토링에 따른 테스트 수정 * chore: 불필요한 로그 제거 * fix: 댓글 입력 후 뒤로가기 시 최근 댓글이 반영되도록 수정 (#397) * feat: 댓글방 참여자 확인 API 연결 (#401) * feat: 참가자 정보를 가져오는 api service 구현 * refactor: 필요없는 코드 삭제 * feat: 참여 관리 datasource 구현 * feat: 참여자 domain 모델 구현 * feat: 참여를 관리하는 repository 구현 * feat: 참여자 목록을 보여주는 recycler view 연결 및 구현 * refactor: 더보기 버튼 수정 * feat: 필요없는 리소스 파일 삭제 및 상태 기본 이미지 변경 * refactor: 약속 장소 및 시간 ui model 을 사용하여 관리 * refactor: 댓글방의 정보를 불러오는 로직 ui model을 사용하여 관리 * refactor: ui model 변환 로직 변경 * feat: 공동구매 참여 인원 확인 기능 구현 * feat: 신고하기 폼 연결 구현 * test: 코드 변경에 따른 테스트 코드 수정 * style: ktlint 적용 * refactor: xml id 추가 * feat: 댓글방 공동구매 나가기 API 연결 (#402) * feat: 공동구매 나가기 기능 api service 구현 * feat: 공동구매 나가기 기능 data source 구현 * feat: 공동구매 나가기 기능 repository 구현 * feat: 공동구매 나가기 기능 연결 * style:ktlint 적용 * feat: CallApiHandler 구현 (#403) * feat: CallApiHandler 구현 * refactor: CommentRoomsDataSource 수정 * feat: CommentRemoteDataSourceImpl 에러핸들링을 통해 수정 * feat: 에러 핸들링에 따른 DataSource 리팩토링 - OfferingDetailDataSource - OfferingRemoteDataSource * feat: ParticipantRemoteDataSourceImpl 에러핸들링을 통해 수정 * style: ktlint 적용 * refactor: AuthRemoteDataSource 수정 * feat: Result의 map 과 getOrThrow 함수 생성 * feat: 에러 핸들링에 따른 Repository 리팩토링 - OfferingDetailRepository - OfferingRepository * refactor: Result 변경에 따른 레포지토리 수정 (AuthRepository, CommentRoomsRepository) * feat: 에러 핸들링에 따른 CommentDetailRepository 리팩토링 * feat: 에러 핸들링에 따른 ParticipantRepository 리팩토링 * feat: 에러 핸들링에 따른 viewmodel 리팩토링 - OfferingViewModel - OfferingDetailViewModel * refactor: 에러 핸들링에 따른 LoginViewModel 리팩토링 * refactor: 에러 핸들링에 따른 CommentRoomsViewModel 리팩토링 * refactor: 토큰 리프레쉬 후 다시 함수 호출하도록 추가 * feat: 에러 핸들링에 따른 CommentDetailViewModel 리팩토링 * refactor: 에러 핸들링에 따른 OfferingWriteViewModel 리팩토링 * refactor: 공모 목록 토큰 리프래시 적용 * fix: 잘못된 코드 수정 * refactor: 필요없는 주석 제거 * refactor: 공모 목록 리팩토링 * fix: 리빌드시 쿠키가 제대로 저장되지 않는 현상 수정 * refactor: 필요없는 코드 삭제 및 상수화 추가 * test: 에러핸들링에 따른 FakeAuthRepository, OfferingWriteViewModelTest 수정 * refactor: ktFormat 적용 * test: 코드 변경에 따른 Fake Repository 변경 * test: CommentDetailViewModelTest 코드 수정 * style: ktlint 적용 * refactor: 가독성 개선(에러 로그 함수명 추가, Success가 Error보다 위에 나오도록 수정) * refactor: 불필요한 로그 제거 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 람다 넘겨주는 방식 수정 * style: lint 적용 * test: 테스트코드 수정 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: Namyunsuk * feat: proguard를 사용한 난독화 적용 (#413) * fix: 공구 상세 페이지 오류 해결 (#417) * fix: 바로가기 클릭되지 않는 오류 수정 * refactor: 주소 표시할 때 최대 2줄까지 그리고 넘어갈 시 말줄임 나오도록 수정 * refactor: 공모 목록, 공모 상세 에러 핸들링 (#418) * refactor: 공모 목록에서 401에러를 제외하고는 에러코드 올 시 빈화면 보여주도록 에러핸들링 수정 * refactor: 필터및 업데이트된 공모 목록 가져오는 로직 에러핸들링 수정 - 400: 토스트 메시지 띄어줌 - 401: refresh - 그외에는 로그로 에러 코드를 보여줌 * refactor: strings네이밍 통일 * refactor: 공모 상세 에러 핸들링 수정 * refactor: strings정리 - offering_detail부분 정리 * feat: 로그인 api 변경 반영 (#426) * feat: 카카오 로그인 후 총대마켓 서버로 email을 보내던 방식에서 카카오 access token을 보내는 방식으로 변경 * feat: login과 signup을 하나로 api로 통합된 것 반영 * refactor: ktFormat 적용 * refactor: 테스트코드 수정 * refactor: 에러 핸들링 리팩토링 (#436) * feat: 리프레시 토큰 만료 시 데이터스토어를 비우고 로그인 화면으로 이동하는 기능 구현 (#438) * feat: 댓글방 에러 헨들링 (#439) * refactor: refresh시 401이 오는 경우에 대한 에러핸들링 추가 (#441) * chore: 버전 업데이트 (#443) * v1.1.0 (#448) * feat: 게시글 상세 화면 구현 (#8) * feat: 게시글 상세 화면 레이아웃 작성 * feat: Data layer코드 작성 * refactor: dto패키지 분리, dto에 serialName추가 * refactor: 도메인 모델 수정 - 가변에서 불변으로 변경 - 사용하지 않는 메서드 제거 * refactor: 공통으로 사용되거나 사용될 수 있는 확장함수를 별도의 파일로 분리 * style: lint 적용 * refactor: 메서드명 컨벤션 적용 * refactor: request Dto에 SerialName적용 * refactor: 메서드명 수정 * feat: 도메인 추가 (#15) * feat: BaseTimeEntity 추가 Co-authored-by: Dora Choo * feat: Member Entity 추가 Co-authored-by: Dora Choo * feat: Offering Entity 추가 Co-authored-by: Dora Choo * feat: OfferingMember Entity 추가 Co-authored-by: Dora Choo * feat: Comment Entity 추가 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: BottomNavigation 구현 (#16) * chore: jetpack navigation 라이브러리 추가 * feat: 필요한 바텀 네비게이션 리소스 추가 * feat: bottom navigation fragment 추가 * feat: bottom navigation graph 구현 * refactor: 컨벤션에 맞게 id 수정 * feat: 공동구매 상세 조회 기능 구현 (#18) * chore: h2 환경설정 추가 * docs: http client 추가 * refactor: entity 접미어 적용 * chore: dummy data 추가 * docs: http client 값 변경 * refactor: repository 와 domain 패키지 분리 * feat: 공동구매 상세 조회 API 구현 * refactor: entity 접미어 적용 * style: 클래스 컨벤션 적용 * chore: h2 console 설정 제거 * refactor: OfferingCondition enum값 결정로직을 enum 안으로 이동 * feat: 홈화면, 마이페이지 화면 레이아웃 작성 (#19) * refactor: FragmentContainer width 속성 수정 * feat: 홈 화면 레이아웃 작성 * feat: 마이페이지 화면 레이아웃 작성 * fix: 플로팅 버튼이 홈에서만 보이도록 수정 * refactor: 리소스 네이밍 컨벤션에 맞게 수정 * feat: API 문서화 적용 (#23) * chore: springdoc-openapi 의존성 추가 Co-authored-by: Dora Choo * chore: springdoc 설정 추가 Co-authored-by: Dora Choo * feat: SwaggerConfig 파일 추가 Co-authored-by: Dora Choo * feat: 공모 상세 조회 API 문서화 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * fix: 공모 상세 조희 API의 price 필드 자료형 변경 및 memberId 필드 추가 (#28) * fix: 상세조회 API 금액 필드 자료형 변경 Co-authored-by: Dora Choo * fix: memberId 추가 * 내가 쓴 글인지 아닌지 확인 위해 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * chore: 백엔드 CI 및 도커 파일 작성 (#27) * chore: actions 적용 브랜치 설정 (#30) * chore: actions 적용 브랜치 설정 * chore: path 및 ref 태그 제거 * chore: working-directory 태그 추가 * chore: Dockerfile jar 경로 수정 * feat: 댓글방 목록 구현 (#26) * feat: 댓글방 목록 UI 구현 * fix: 구분선을 ImageView에서 View로 변경 * feat: 댓글방 목록 도메인 모델 구현 * feat: 댓글방 어답터 구현 * feat: "채팅" string 추가 * refactor: 불필요한 코드 제거 * fix: xmls 중복 속성 제거 * refactor: 댓글방 클래스들을 comment 패키지로 분리 * refactor: 컬러와 폰트 사이즈를 values 파일로 분리 * feat: 공모 목록 조회 기능 구현 (#35) * feat: 공모 목록 조회 API 구현 * docs: 공모 목록 조회 API http client에 추가 * fix: 공모 상세 조회 API의 status 필드를 condition으로 명칭 변경 * feat: 공모 목록 조회 API의 isClosed 필드 이름을 isOpen으로 변경 * feat: 댓글방 디테일 화면 구현 (#32) * feat: font 설정 * feat: vector 이미지 추가 * feat: 채팅 아이템 뷰 구현 * refactor: 컨벤션에 맞게 네이밍 수정 * feat: 댓글 입력 edit text 구현 * chore: 백엔드 CD 스크립트 작성 (#34) * chore: 백엔드 CD 스크립트 작성 * chore: 도커 백그라운드로 실행 * chore: 도커 설정 및 트리거 설정 변경 * chore: 도커 이미지 제거 로직 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 강제 제거하도록 수정 * chore: gradle 캐싱 로직 추가 (#39) * chore: gradle 캐싱 로직 추가 * chore: 이벤트 트리거 조건 수정 * feat: 공모 참여하기 기능 구현 (#40) * fix: BaseTimeEntity 적용 오류 수정 Co-authored-by: Dora Choo * feat: 참여하기 API 구현 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: 공모 상세 조회 API에 참여자 목록 필드 추가 (#42) * feat: 공모 상세 조회 API의 request에 memberId 필드 추가 (#45) * feat: 공모 참여 API의 불필요한 응답값 전부 제거 (#48) * feat: 공모 참여 API의 불필요한 반환값 제거 * chore: 자주 쓰는 h2 console enabled 설정 주석 처리 * feat: 이미 참여한 공모에 참여 못하게 예외 처리 (#51) * feat: 공모 상세 페이지 API 연결 (#46) * build: 불필요한 의존성 제거, properties관련 코드 작성 * refactor: base_url코드상에서 제거 * feat: api수정에 따른 필드 변경 및 네이밍 반영 * refactor: 네이밍 변경 * refactor: OfferingDetail의 변경, mapper변경 * refactor: service분리 * refactor: DataSource, Repository분리 * refactor: API변경에 따른 리팩토링 * feat: 공모 상세 조회 기능 구현 * refactor: 참여하기 api변경에 따른 data, domain 코드 수정 * feat: 공모 상세 페이지 참여하기 기능 구현 * feat: 공모 상세 화면에서 이미지를 불러올 수 없을 시 기본이미지를 보여주는 기능 구현 * feat: 게시물 상세 화면 폰트 적용 * style: lint적용 * refactor: 액티비티 destroy시 binding해제하도록 코드 추가 * refactor: glide옵션 변경 - 에러 발생 시 보여줄 이미지 - url이 null일 시 보여줄 이미지 * refactor: viewModel에 custom getter추가 * fix: 내용이 짧을 시 뒷 배경이 회색으로 보이는 버그 수정 * fix: 참여하기 버튼을 눌렀을 시 텍스트가 바뀌지 않는 버그 수정 * feat: 테스트 데이터 다양화 (#52) Co-authored-by: Dora Choo * refactor: 공모 엔티티에 currentCount 필드 추가 (#55) * feat: 댓글 작성 API 구현 (#57) * feat: 댓글방 내 공모 일정 조회 기능 구현 (#58) * feat: 댓글방 내 공모 일정 조회 기능 구현 Co-authored-by: Dora Choo * refactor: 공모 일정 조회 api 명세 변경 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * refactor: common 패키지명을 global로 변경 (#61) * chore: 안드로이드 CI 파일 작성 (#63) * feat: 댓글 목록 조회 API 구현 (#66) * chore: build CI 작업을 위한 manifest 파일 수정 (#65) * chore: 알람 권한 추가 * chore: local properties 속성 추가 * chore: local properties null 체크 로직 추가 * chore: buildConfigField null 체크 * style: lint 적용 * chore: secret 값 설정 * fix: secret 값 오류 수정 * fix: 문법 오류 수정 * chore: 경로 수정 * chore: 문법 수정 * style: lint 적용 * feat: 댓글방 목록 조회 API 구현 (#70) * feat 댓글방 접히는 공지 뷰 구현 (#72) * chore: manifest에 CommentDetailActivity 추가 * feat: BindingAdatper을 사용하여 접힐 때 애니메이션 적용 및 픽셀 변환 * feat: viewmodel 구현 및 click 마다 접히고 펴지는 로직 구현 * style: ktlint 적용 * refactor: binding adpater을 사용하여 가시성 변경 * refactor: 댓글방 및 댓글 목록 조회 서비스 계층 (#78) * fix: 댓글방 목록 조회 시 가장 최근 댓글 조회 (#80) * feat: 홈화면 API 연결 (#74) * refactor: API변경에 따른 data, domain 코드 변경 * feat: 공모 목록 기능 구현 * refactor: 함수 분리 * style: lint적용 * style: font 적용 * fix: 시간순 정렬 쿼리 추가 (#83) * chore: 더미 데이터 추가 (#87) * feat: 댓글방 목록 API 연결 (#82) * feat: bottom navigation fragment 추가 * feat: vector 이미지 추가 * feat: 댓글방이 없으면 "채팅 목록이 없어요" 라는 텍스트뷰와 이미지뷰를 띄우는 기능 구현 * feat: 댓글방 띄우는 기능 구현 * test: 댓글방 UI 테스트 작성 * refactor: 테스트 클래스명 수정 * refactor: 줄바꿈 수정 * feat: 댓글방 API 서비스 구현 * refactor: API 명세에 따라 도메인 모델 수정 * feat: API 연결 * refactor: API명세에 따라 데이터바인딩 변수명 수정 * feat: 댓글방 목록 API 연결 * refactor: ktlint Format 적용 * refactor: 메모리 누수 방지를 위해 fragment가 destroy 될 때 _binding을 null로 설정 * refactor: 어답터를 방어적복사 하지 않아도 되어서 수정 * refactor: 채팅방이 없다는 이미지뷰를 띄워주는 방식 수정(바인딩 어댑터 수정) * refactor: 함수분리 * refactor: ktFormat 적용 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> * feat: 댓글방 접히는 공지 API 연결 (#85) * feat: 미팅 일정 API 연결을 위한 data layer 구현 * feat: 미팅 일정 API 연결을 위한 domain layer 구현 * feat: 미팅 일정 API 연결을 위한 presentation layer 구현 * style: ktlint 적용 * feat: 공동 구매 제목 databinding 적용 * refactor: 변수명 수정 * fix: 펼치기 접기 버튼 로직 반대로 수정 * style: ktlint 적용 * chore: 더미 데이터 바로가기 url 수정 (#93) * feat: 공모 상세 페이지 기능 추가 (#94) * chore: 마이페이지 닉네임 임시로 지정 * feat: 바로가기 기능 구현 * feat: 참여버튼 클릭 시 댓글방으로 가도록 기능 구현 * feat: 신고하기 이미지 추가 * style: lint적용 * refactor: 불러오는 공모 페이지 사이즈 변경 * refactor: 댓글 도메인 코드 리팩터링 (#96) * refactor: 로그인 멤버 변수명 변경 * refactor: JPQL 쿼리 컨벤션 및 멤버로 공모 조회 메서드명 변경 * refactor: 최근 댓글 응답 클래스명 변경 * refactor: 컨트롤러 및 서비스 API 순서 변경 * refactor: 로그인 사용자 유효성 검증 * feat: 댓글방 댓글 작성 api 연결 (#95) * chore: windowSoftInputMode 추가 * feat: post comment api service 구현 * feat: post comment DataSource 구현 * feat: post comment Repository 구현 * feat: post comment Presentation 구현 * chore: 더미 데이터 시간 변경 (#100) * feat: 댓글방 입장 기능, 본인이 총대인 방은 다르게 보이는 기능 구현 (#99) * feat: 댓글방의 마지막 댓글 시간을 띄우는 기능 구현 * feat: 자신이 총대인 댓글방을 표시하는 기능 구현 * feat: 댓글방 목록을 클릭해 댓글방 상세로 이동하는 기능 구현 * test: UI테스트 수정 * refactor: 클릭시 id 뿐만 아니라 title도 받아오는 방식으로 수정 * refactor: 오전/오후와 시간을 텍스트뷰에 띄우는 바인딩 어댑터를 DateTimeFormatter의 기능을 사용하는 것으로 수정 * refactor: memberId를 local.properties의 token을 가져다 쓰는 것으로 변경(임시 조치) * refactor: 댓글방 목록의 시간을 띄우는 바인딩 어댑터의 속성명을 수정함 * refactor: 데이터바인딩 variable 변수명을 구체적으로 수정, 일관성을 위해 앞에 `on` 붙임 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정(빠트린것 수정함) * feat: 전반적인 예외 처리 (#103) * feat: 예외 처리 핸들러 추가 * feat: Offering 예외 처리 코드 추가 * feat: Comment 예외 처리 코드 추가 * feat: Member 예외 처리 코드 추가 * feat: OfferingMember 예외 처리 코드 추가 * feat: Offering 예외 처리 상세 코드 추가 * feat: 에러 코드 적용 * feat: 도메인 검증 로직 * feat: DTO 검증 로직 --------- Co-authored-by: masonkimseoul * feat: swagger와 restdocs 연동 (#104) * chore: swagger ui 정적 파일 설치 및 static routing 세팅 * chore: restdocs-api-spec을 이용한 OAS 생성 * chore: swagger ui 정적 파일을 swagger-ui 디렉토리로 이동 * chore: swagger ui 정적 파일 및 static routing 세팅 제거 * chore: 생성된 OAS 파일을 Swagger 디렉터리로 복사하는 스크립트 작성 * chore: openapi3 yaml 파일 gitignore 처리 * chore: static routing 세팅 다시 추가 openapi3.yaml을 사용하기 위함 * test: RestAssured RestDocs 테스트 코드 작성 * test: 공모 목록 조회 API 문서화 * test: 공모 일정 조회 API 및 공모 참여 API 문서화 * test: 댓글 관련 API 문서화 * docs: 논의된 TODO 제거 * refactor: swagger 어노테이션 제거 * chore: 개발 API 서버 목록 설정 --------- Co-authored-by: fromitive * refactor: 에러메시지 필드명 변경 (#108) * fix: restdocs 관련 테스트 실패 이슈 해결 (#106) * chore: cicd 테스트 * chore: 테스트 위해 actions 범위 조정 * chore: 배포 스크립트 띄어쓰기 오타 수정 * chore: 빌드 캐싱 제거 * chore: logging * chore: --warning-mode all 옵션 줘서 gradle 호환 무시하도록 설정 * fix: status 달라서 실패하는 테스트 수정 * chore: actions 범위 수정 * chore: action 범위 수정 * chore: test용 static 파일 추가 * chore: static 하위 폴더를 jar 파일에 포함하도록 설정 * chore: swagger-ui 하위 폴더 제거 * chore: task 순서 조정 * chore: build 스크립트 수정 * chore: 불필요한 설정 변경 제거 * chore: clean build 대신 clean bootJar 사용 * chore: clean, build 각각 하도록 변경 * chore: test 까지 두 번 돌리도록 수정 * chore: openapi3까지 두 번 실행하도록 수정 * chore: copyOasToSwagger 까지 두번 실행하도록 수정 * chore: actions 활성화 범위 수정 * fix: 댓글방 목록 조회 시 참여자 수 조건 추가 (#111) * fix: 댓글방 조회 테스트 수정 (#113) * feat: 홈 화면 무한 스크롤 기능 구현 (#109) * build: pagination라이브러리 추가 * feat: 홈 화면 무한 스크롤 기능 구현 * fix: 마지막 댓글 response를 nullable하게 수정 (#115) * fix: 마지막 댓글 response를 nullable하게 수정 * refactor: ktFormat 적용 * feat: 댓글방 댓글 조회 api 연결 (#116) * feat: dto 및 mapper 구현 * feat: 댓글방 목록 service 구현 * feat: 댓글방 목록 data source 구현 * feat: 댓글방 목록 repository 및 model 구현 * feat: 댓글방 목록 view type을 활용한 recyclerview 구현 및 데이터 바인딩 * feat: polling 기능 구현 * feat: 댓글 스크롤 구현 (새로운 댓글이 생길시 스크롤 아래로) * feat: 총대와 다른 참가자 이미지 리소스 파일 * feat: 댓글방 디테일 공동 구매 상태별 관리 (#117) * feat: 공동구매 상태 관리 리소스 파일 * feat: 공동구매 상태를 관리하는 enum class 구현 * feat: 데이터바인딩을 사용하여 공동 구매 상태 뷰 업데이트 구현 * style: ktlint 적용 * feat: 공동구매 상태 관리 리소스 파일 추가 * fix: 이미지 링크 임시 수정 (#119) * fix: 이미지 링크 수정 (#120) * refactor: 네이밍 수정 (#123) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 (#125) * refactor: 뷰모델 팩토리 방식 변경 (#130) * refactor: 뷰모델 팩토리를 뷰모델의 동반객체로 이동 * style: lint적용 * refactor: Service분리 (#132) * refactor: service분리 * refactor: 패키지명 변경 * style: lint적용 * feat: 공모글 작성 UI 구현 (#134) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 뷰 구현 * fix: 뷰 수정사항 반영 * fix: @+id로 참조하는 부분을 수정 * fix: drawable의 네이밍에 where을 추가 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 (#136) * feat: 참여자 목록 drawer에 필요한 리소스 파일 추가 * refactor: 채팅 text gravity 수정 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 * style: ktlint 적용 * refactor: drawer early return 하는 방식으로 변경 * refactor: ivMore -> ivMoreOptions으로 네이밍 변경 * feat: 공구 참여자 item view 및 댓글방 view 사용자 친화적으로 수정 * chore: CI 빌드 스크립트 중 중복되는 task 제거해 성능 개선 (#128) * chore: jar태스크 비활성화하고 bootJar 태스크로만 JAR 파일 생성 * chore: cicd 범위 조정 * feat: 공모 작성 API 구현 (#139) * feat: 공모 작성 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: create를 save로 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: dto entity 매핑로직을 dto로 이동 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: controller request 매개변수 명 컨벤션 적용 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 공모에 저장하는 주소 값 구체화 (#141) * refactor: 공모에 저장하는 주소 값 구체화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: github-action 스크립트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: CI/CD test 설정 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: static/swagger-ui 폴더 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: 설정 원상 복구 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: ci/cd 범위 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 홈화면(공모목록) UI 추가 구현 및 상태 변경 대응 (#142) * feat: 공모의 상태 변경이 반영되도록 기능 구현 * feat: 공모 목록 ui변경 * feat: 필터 ui추가 * feat: API변경에 따른 DTO수정 * style: lint적용 * feat: resource추가 * refactor: ui위치 수정 * chore: 불필요한 괄호 제거 * refactor: item 수직 정렬 * feat: 댓글방 메시지 조회 시 commentId 필드 추가 (#150) Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: OG 태그 크롤링 API 구현 (#148) * feat: OG 태그 크롤링 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: OG 태그 크롤링 API 엔드포인트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 제품 코드와 API 문서 동기화 (#153) * refactor: API 문서 개선 (#157) * refactor: 댓글 작성 시 성공 상태 코드 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 요청 필수 상태 설명 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: s3 이미지 업로드 API 구현 (#147) * feat: s3 이미지 업로드 API 구현 * chore: cicd 액션 범위 수정 * fix: 이미지 업로드 경로의 특수문자 제거 * chore: yml multipart 설정 추가 * chore: S3 업로드 결과 테스트 * fix: inputstream 변환로직 위치 이동 * fix: 업로드할 s3 path 올바르게 수정 * fix: 사진 url 속에 버킷이름을 cloudfront 도메인으로 수정 * chore: actions 범위 재조정 * feat: API endpoint 변경 * chore: docker image 지우는 작업을 마지막으로 이동 * chore: 다른 브랜치로 이전 커밋 이동하기 위해 제거 * chore: 충돌 해결 및 코드 스타일 변경 * test: S3 이미지 업로드 성공 케이스 추가 * test: multipart form data 문서화 * test: 공모 상태 enum 문서화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 파일 업로드 크기 제한 100MB에서 20MB로 변경 --------- Co-authored-by: Choo Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 주소검색 기능구현 (#161) * refactor: 네이밍 컨벤션 적용 * build: webview 라이브러리 추가 * feat: 스크립트 실행위한 html파일 추가 * refactor: 인터페이스명 변경에 따른 변경 * feat: 주소검색 다이얼로그 레이아웃 작성 * feat: 주소검색 기능 구현 * style: lint적용 * refactor: 불필요한 코드 제거 * build: Firebase의존성 추가 (#165) * feat: 공모글 작성 API 연결 (#162) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 API 연결 구현 * feat: 공모글 작성 뷰모델 구현 * fix: edit text 데이터바인딩 추가 * chore: 테스트를 위해 MutableLiveData default값 넣어둠 * chore: deadline defualt값 형식에 맞게 수정 * feat: 글작성 화면을 액티비티에서 프래그먼트로 수정 * chore: 테스트목적이었던 주석과 mutable livedata 디폴트값 제거 * refactor: 임시 함수명 수정 * fix: 글작성 프래그먼트가 올라오기 전에 바텀 네비게이션이 사라지는 문제 수정 * feat: 필수 항목이 모두 입력되어야 버튼이 활성화 되는 기능 구현 * feat: 가격, 총원 입력이 잘못되었을 시 토스트를 띄우는 기능 구현 * fix: 버튼 비활성화 시 텍스트 변경 * feat: 앱 아이콘 변경 * feat: 앱 이름 변경(chongdae -> 총대마켓) * feat: 예상 엔빵 가격을 보여주는 기능 구현 * refactor: 상수화 * refactor: 예상 엔빵 가격에 ,가 들어가는 기능 구현, 콜론 뒤 white space 추가 * feat: 공구 할인율을 계산해 주는 기능 구현 * feat: +, - 버튼으로 총원을 조절하는 기능 구현 * fix: 할인율과 엔빵가격 계산 시 0으로 나눠지는 상황을 제거 * fix: 맞춤법 수정 할인률 -> 할인율 * fix: 총원 버튼 크기가 너무 작아서 확대 * fix: 항목간 간격이 좁아서 확대 * refactor: Offering Write의 API service, DataSource, Repository를 Offerings와 합침 * refactor: 디버깅용 코드 삭제 * refactor: 버튼 활성화/비활성화를 selector와 삼항연산자로 구현 * refactor: 바인딩어댑터 대신 뷰모델이 visibility 상태를 갖고 있는 방식으로 변경 * refactor: 바인딩어댑터 대신 xml에서 처리하는 방식으로 변경 * refactor: 총원 디폴트 라이브데이터값 상수화 * refactor: +, - 텍스트뷰 버튼으로 수정 * refactor: textStyle bold대신 fontFamily suit_bold를 쓰는 것으로 수정 * refactor: 변수명 뒤에 Int를 붙이는 것 대신 Value를 붙이는 것으로 수정 * refactor: 글작성 제출 버튼의 아이디를 추가 * refactor: ktFormat * refactor: 토스트를 띄우는 함수 분리 * refactor: 도메인 객체 분리 * refactor: UI모델 적용 * refactor: ktFormat 적용 * feat: 댓글방 디테일 Room을 사용하여 data 저장 (#166) * feat: local database 구현 * feat: entity 구현 * feat: dao 구현 * feat: LocalDataSourceImpl 구현 * feat: entity mapper 구현 * refactor: CommentResponse 에 id 값 추가 * refactor: datasource 이름 변경 및 패키지 변경 * refactor: article -> offering으로 네이밍 변경 * refactor: repository 패키지 변경에 따른 수정 * refactor: datasource 패키지 변경 및 local 과 remote 분리 * refactor: repository Application 클래스를 통한 주입으로 변경 * style: ktlint 적용 * refactor: api service 리네이밍 * refactor: git conflict 해결 * refactor: 함수 이름 컨벤션에 맞도록 변경 (getMeetings -> fetchMeetings) * chore: CI 스크립트 추가 (#173) * chore: ci 스크립트 추가 * chore: ci 스크립트 수정 * fix: og 태그 추출 시 크롤링 이슈 해결 (#174) * feat: 날짜, 시간 선택 기능 구현, 주소검색 기능 연결 (#171) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 모집마감 시간 클릭 시 date time picker를 띄우는 기능 구현 * feat: 날짜, 시간 선택 기능 구현 * feat: 주소 검색 기능 연결 * refactor: 함수명 수정, 함수분리 * refactor: ktFormat 적용 * refactor: string으로 분리, 상수화 * fix: string 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정3 * chore: CI workflow 파일 수정4 * feat: 공모가 정상적으로 게시되었을 시 "공모가 게시되었어요!" 라는 토스트를 띄우고 공모글 작성 프래그먼트를 종료하는 기능 구현 * feat: 토스트가 화면 중앙에 뜨는 문제 수정 * refactor: 사용되지 않는 파일 삭제 * refactor: xml 뷰 id 수정 * refactor: 버튼이 TextView인 문제 수정 * refactor: 사용되지 않는 data binding variable 제거 * refactor: 함수명 수정 * refactor: 다이얼로그, dateTimePickerBinding 전역으로 선언 * refactor: dateTimePicker 클릭 이벤트를 추상화 해 xml에서 처리하도록 변경 * refactor: ktFormat * feat: 상품 URL 이미지 추출 API 연결 (#180) * refactor: 사용하지 않는 파일 제거 * refactor: 가시성 변경 * feat: api service 구현 * feat: datasource 구현 * refactor: repository 네이밍 수정 (offeringsRepository -> offeringRepository) * feat: 사진 업로드 관련 리소스 파일 추가 * feat: repository 및 model 구현 * feat: 이미지 링크를 통한 크롤링 이미지 불러오는 api 연결 및 이미지 삭제 로직 구현 * style: ktlint 적용 * refactor: 이미지 prefix 추가 및 에러 메시지 수정 * refactor: build 오류 수정 * fix: git conflict 해결 * feat: 공모 목록 조회 API에 필터링과 검색 기능 추가 (#169) * feat: 공모 필터 목록 조회 API 구현 * test: 공모 필터 목록 조회 API 테스트 * style: 개행 형식 통일 * feat: 공모 필터 목록 조회 API Specification 도입 준비 * fix: url에 큰따움표 제거 * feat: Specification 도입 * refactor: queryString 구체화 * refactor: 함수명 변경 * feat: 최신순 필터링 적용 * feat: 마감임박순 필터링 적용 * feat: 높은할인률순 필터링 적용 * refactor: 전략 패턴 적용해 여러 갈래의 분기문과 중복되는 코드 처리 * test: 변경된 API 스펙에 맞게 문서화 작업 * refactor: 관련있는 메서드들끼리 모이게 순서 재배치 * refactor: 맞춤법 수정 * style: 개행 제거 --------- Co-authored-by: masonkimseoul * feat: 상태 변경 API 구현 (#175) * feat: 댓글방 상태 변경 및 조회 API 구현 Co-authored-by: masonkimseoul * feat: 공모글 상태 조회 API 구현 * feat: 댓글방 상태 변경 중 수동 확정 기능 구현 * refactor: 상태 변경 관련 메서드명 수정 * refactor: 추상 클래스 메서드 컨벤션 통일 * refactor: errorCode 사용 시 클래스 명시 * refactor: 댓글방 상태 관련 API 엔드포인트 수정 및 패키지 변경 * refactor: 댓글방 상태 변경 API HTTP 메서드 수정 * feat: 공모 모집 자동 확정 시 댓글방 상태 변경 --------- Co-authored-by: masonkimseoul Co-authored-by: Choo * feat: 로그인 기능 구현 (#177) * feat: password 일방향 암호화 기능 구현 * feat: cookie 생산-소비 기능 구현 * chore: jwt 관련 의존성 추가 * feat: 토큰 생성 기능 구현 * feat: 로그인 API 구현 * test: 로그인 API 테스트 * feat: 회원가입 API 구현 * test: 회원가입 API 테스트 * feat: 닉네임 생성 기능 구현 * test: 닉네임 생성 기능 테스트 * fix: postconstruct 여러 개라 발생한 에러 해결 * feat: 회원가입 응답값에 랜덤생성한 닉네임 추가 * feat: MemberArgumentResolver 구현 * feat: MemberArgumentResolver 일부 적용 * test: 바뀐 스펙에 맞게 변경 * test: TestConfig 설정해 빈충돌 오류 해결 * test: 공모 작성 API로 MemberArgumentResolver 사용 * feat: 토큰 재발급 API 구현 * test: 토큰 재발급 API 테스트 * test: 토큰 재발급 API 에러 테스트 * feat: MemberArgumentResolver commant에 적용 * feat: MemberArgumentResolver offering에 적용 * feat: MemberArgumentResolver participant에 적용 * refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경 * refactor: 클래스명 일관적으로 변경 * refactor: 직관적인 명명으로 enum 네이밍 변경 * refactor: Custom Exception 적용 * refactor: 컨트롤러 메서드에 접근제어자 명시 * fix: 중복된 enum 값 제거 * test: 바뀐 API 스펙에 맞게 변경 --------- Co-authored-by: fromitive * fix: nicknameWordInitializer 설정 오류 해결 (#182) * fix: keyword null일 때 처리 및 docs에서 required 제거 (#184) * fix: keyword null일 때 처리 * test: optional() 붙여서 required 제거 * chore: 브랜치에 상관없이 pr 머지 시 자동으로 관련 이슈 닫는 스크립트 구현 (#187) * fix: og 이미지 태그 크롤링 문제 해결 (#190) * refactor: 댓글방 상태 도메인 설계 변경 (#189) * feat: 공모 목록 API 응답값에 낱개 가격 추가 (#193) * chore: readtimeout 5초로 수정 (#195) * feat: 댓글방 상태 조회 시 상태별 이미지 함께 반환 (#196) * feat: 공모 목록 조회 API연결 (#201) * refactor: Condition 수정에 따른 변경 * refactor: api변경에 따른 리팩토링 * refactor: api변경에 따른 목록 무한 스크롤 기능 리팩토링 * feat: 검색 기능 구현 * feat: 필터링 기능 구현 - 참여 가능은 서버 에러로 추후 추가 예정 * feat: 아이템을 불러온 후 recyclerview의 최상단으로 이동하는 기능 구현 - 검색, 필터링 수행 후 최상단으로 이동 * feat: 필터링 목록 불러오는 api연결 * feat: 마감임박 상태 추가 * refactor: default parameter제거 * style: lint적용 * feat: 토큰 반환 시 cookie가 아닌 body 사용하도록 변경 (#206) * feat: 발급한 토큰을 header가 아닌 body로 반환하도록 수정 * refactor: 사용안하는 클래스와 메서드 제거 * test: 바뀐 API 스펙에 맞게 명세 수정 * feat: 이미지 더미 데이터 수정 및 부정확한 가격 데이터 수정 (#207) * refactor: 공모 글 작성 시 총대 참여자 추가 (#208) * feat: 바텀 네비게이션 고정 기능 구현 (#211) * feat: 데이터에서 5자 이상 제거 (#212) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 (#202) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 * refactor: 도메인 명칭 변경 (낱개가격 -> 원가격) * refactor: 도메인 명칭 변경 (공모 -> 댓글방) * refactor: originPrice로 http client 변경 * feat: 키보드 이외 영역 터치 시 키보드 내려가도록 구현 (#214) * feat: 키보드외 화면 클릭 시 키보드 내려가도록 구현 * refactor: api변경에 다른 dto수정 * feat: 이미지 업로드 및 권한 설정 (#216) * chore: 이미지 권한 추가 * feat: permission manager을 생성하여 권한 체크 및 request * feat: 이미지 추가 버튼을 클릭할 시 권한 설정 연결 * feat: 이미지 피커를 사용하여 uri 전달 구현 * feat: 이미지 파일 업로드 api service 구현 * feat: 이미지 파일 업로드 data source 구현 * feat: 이미지 파일 업로드 repository 구현 * feat: 이미지 파일 martipart로 변환해주는 기능 구현 * feat: 이미지 업로드 관련 뷰 수정 * feat: 이미지 파일 업로드 및 api 연결 구현 * style: ktlint format * fix: git conflict 해결 * refactor: 이미지 scaleType 변경 * refactor: string value 컨벤션 적용 * feat: 토큰 반환 시 body가 아닌 cookie로 반환하도록 원상복구 (#223) * feat: 토큰 재발급 API에서 requestHeader로 refreshToken 받도록 수정 (#227) * feat: 토큰 재발급 API에서 body가 아닌 cookie로 토큰 반환 * feat: 회원가입 API도 body가 아닌 cookie로 토큰 반환 * refactor: service 용 dto 명 컨벤션에 맞춰 수정 * feat: 댓글방 일정 수정 API 구현 (#226) * feat: 댓글방 일정 수정 API 구현 * test: 총대가 아닌 참여자가 공모 일정 정보를 수정할 경우 예외 발생 * feat: 댓글방 상태 조회 시 버튼 텍스트 추가 (#229) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 (#222) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 * refactor: 구현 방식 변경 * style: lint적용 * Feature/217 offering status (#230) * feat: 댓글방 상태 조회 api service 구현 * feat: 댓글방 상태 조회 model 및 dto 구현 * feat: 댓글방 상태 조회 datasource 구현 * feat: 댓글방 상태 조회 repository 구현 * feat: 댓글방 상태 조회 api 연결 구현 * style: ktlint 적용 * feat: 댓글방 상태 변경 (#231) * feat: 댓글방 상태 변경 api service 구현 * feat: 댓글방 상태 변경 data source 구현 * Revert "feat: 댓글방 상태 변경 data source 구현" This reverts commit 052691a8de945c60a60586ee66a05a6a3b264217. * feat: 댓글방 상태 변경 data source 구현 * feat: 댓글방 상태 변경 repository 구현 * feat: 댓글방 상태 변경 api 연결 구현 * style: ktlint 적용 * feature: 카카오 로그인 구현 (#235) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * feat: 공모 참여자 목록 조회 API 구현 (#225) * feat: 공모 참여자 목록 조회 API 구현 * test: 실패 테스트 오류 수정 * style: 띄어쓰기 적용 * refactor: MemberEntity를 받도록 변경 * refactor: isParticipant를 구현하여 가독성 개선 * refactor: 총대를 찾을 수 없는 상황의 예외 추가 * refactor: 참여 검증로직을 서비스로 이동 * refactor: 사용하지 않는 메서드 제거 * refactor: 검증 로직 가장 상단에 위치 * refactor: 총대 추출 로직 수정 --------- Co-authored-by: masonkimseoul Co-authored-by: SCY * refactor: 마감임박순 필터링 쿼리 조건 수정 (#239) * refactor: 마감임박순 필터링 조건 수정 * refactor: 더미 데이터 시간 수정 * fix: 필터링 오류 수정 (#243) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 (#247) * feat: 공동구매 상태 변경 다이얼로그 구현 (#245) * feat: 공동구매 상태 변경 다이얼로그 view 구현 * feat: 공동구매 상태 변경 다이얼로그 Listener 구현 * feat: 공동구매 상태 변경 다이얼로그 연결 및 상태 변경 로직 수정 * test: 테스트 코드 작성을 위한 기본 세팅 (#255) * feat: CoroutinesTestExtension 구현 * feat: Livedata getOrAwaitValue 구현 * feat: InstantTaskExecutorExtension 구현 * feat: TestFixture 생성 * style: ktlint 적용 * feat: 공모글 목록 화면 UI 개선, 공모글 작성에서 낱개 금액이 엔빵 가격보다 저렴할 시 글 작성 막는 기능 구현 (#246) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 구분선을 각각의 아이템의 하단에 넣고 프래그먼트 뷰의 "채팅" 텍스트 밑에 하나 추가 * fix: 텍스트뷰에 font 적용, 마지막 댓글 시간 텍스트를 조금 왼쪽으로 이동 * fix: 낱개 가격 이름을 eachPrice -> originPrice 수정 * fix: 낱개 가격이 엔빵 가격보다 싸면 토스트를 띄우고 글작성을 막는 기능 구현 * fix: 네이티브앱키 로컬프로퍼티로 이동 * refactor: 함수명 변경 * fix: 카카오 계정으로 로그인 후 액티비티 전환하지 않는 문제 수정 * refactor: 사용되지 않는 클래스 삭제 * refactor: 패키지 수정 * refactor: alsong 로그 수정 * refactor: 변수명 수정 * refactor: Manifest의 네이티브앱 키 숨김 * refactor: 로컬프로퍼티의 데이터 형식 수정 * Update android.yml * refactor: alsong 로그 삭제 * ci 빌드 실패가 manifest때문인지 테스트 * refactor: 매니페스트에 앱 키 넣을 수 있게 하는 gradle 설정 수정 * 매니페스트 수정하고 재테스트 * 매니페스트 수정하고 재테스트 * chore: 그래들 수정 * chore: 그래들 수정2 * chore: 그래들 수정3 * chore: 그래들 수정4 * chore: 카카오 계정으로 로그인하는 기능 제외 * feat: 홈화면 테스트 작성 (#257) * chore: mockk의존성 추가 * test: OfferingViewModel 테스트 작성 * style: lint적용 * refactor: stub를 TestFixture로 이동 * test: 댓글방 테스트 코드 작성 (#258) * refactor: 댓글 보내는 함수명 변경 * refactor: 공구 약속 장소 및 시간 캐시 기능 * test: 테스트를 위한 fake repository 구현 * test: 댓글방 viewmodel test 작성 * feat: 댓글방 ActivityTest 작성 * feat: 댓글방 ActivityTest 작성 * style: ktlint 적용 * refactor: test fixture에서 사용하지 않는 것 삭제 * style: ktlint 적용 * feat: GA 모니터링 환경 구축 및 로깅 전략 적용 (#242) * chore: Firebase Crashlytics 의존성 추가 * feat: Firebase 초기화 * feat: FirebaseManager 구현 * feat: 총대가 공구 진행 상황을 다음 단계로 변경했을 때 event 추가 * feat: 로깅 기능 구현 - 검색 - 필터링 - 공모글 클릭 - 공모 참여 * style: lint적용 * feat: 글 작성 완료 시 event 추가 * feat: 로그인 시 event 추가 --------- Co-authored-by: Namyunsuk Co-authored-by: songpink * test: 공모글 작성 이미지 테스트 코드 작성 (#260) * refactor: 상수 가시성 변경 * feat: test fixture 구현 * feat: fake repository 이미지 업로드 기능 추가 * test: OfferingWriteViewModelTest 이미지 업로드 test 코드 작성 * feat: 로그인 후 홈화면으로 이동해도 로그인 화면이 종료되지 않는 문제 수정 (#261) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 로그인 후 LoginActivity가 종료되도록 수정 * feat: 공모 상세 화면 테스트 작성 (#264) * feat: OfferingDetailViewModel 테스트 작성 * refactor: 테스트 수정 * style: lint적용 * style: lint적용 * feat: 로깅 코드 삽입 (#266) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 * feature: 로깅 샘플 구현 * refactor: 불필요한 코드 제거 * feat: logging 적용 --------- Co-authored-by: fromitive * fix: 마감 임박 필터링 쿼리 수정 (#267) * chore: logback 설정 진행 (#270) * chore: logback 설정 * fix: multipart 요청 필터링 * chore: logback 설정 변경 * chore: pull request ci/cd 닫기 * fix: 이미지 업로드 API의 responseBody가 두 번 뜨는 오류 해결 (#273) * fix: 이미지 업로드 API 두 번 도는 문제 해결 * test: 이미지 업로드 API의 누락된 response field 추가 * refactor: 홈화면 수정 (#271) * refactor: 할인율 마진 추가 * refactor: 공구상태에 대한 문구 수정 * refactor: 클릭 시 최상단으로 이동하는 버튼 구현 * feat: 공모글 작성 화면 테스트코드 작성 (#274) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: 공모글 작성 테스트 구현 * feat: 댓글방 목록 화면 테스트코드 작성 (#276) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: "댓글방 목록을 확인할 수 있어야 한다" 테스트 작성 * feat: pageSize validation 추가 (#279) * feat: pageSize validation 추가 * feat: magic number 추출 * fix: 공모 상세 화면 오류 수정 (#280) * fix: 총대 여부 확인 로직 수정 * fix: 마감 임박 시 보여주는 버튼 수정 * fix: 공모 작성 후 홈화면으로 돌아왔을 떄 목록이 새로고침 되지 않는 오류 수정 * test: 테스트 코드 수정 * style: lint적용 * feat: 댓글방 목록 화면 자동 업데이트 되지 않는 문제 수정, 회원가입 이후 자동으로 로그인되지 않는 문제 수정 (#282) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 라이플사이클 오너 설정 * fix: 회원가입 후 자동으로 로그인 되도록 수정 * chore: change version name (#291) * feat: 카카오 계정 로그인 기능 구현 시 CI가 실패하는 문제 해결 (#296) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * feat: 로그인 화면 리팩토링 (#298) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * refactor: SimpleCookieJar의 패키지 변경(presentation 레이어에서 data레이어의 source 패키지로 이동) * refactor: data store를 관리하는 클래스를 생성하고 이 클래스를 사용하도록 변경 * refactor: 사용하지 않는 의존성과 주석 제거 * refactor: http status code 추가 * refactor: 함수분리 * refactor: ktFormat 적용 * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentRooms) * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentDetail), 사용되지 않게 된 memberId 제거 * refactor: ktFormat 적용 * test: 테스트코드 수정 * refactor: Preferences -> DataStore 이름 변경 * refactor: 채팅방 UI UX 개선 (#303) * feat: 키보드가 아닌 다른 영역을 클릭하면 키보드 내리는 기능 구현 * feat: 뒤로가는 버튼 기능 추가 * feat: 댓글 입력 maxLines 설정 및 maxLength 설정 * style: ktlint 적용 * 필요 없는 코드 제거 * feat: 댓글방 목록에서 자신이 총대인 댓글방의 UI 개선 (#304) * refactor: 댓글방의 자신이 총대인 댓글방 ui 개선 * fix: Binding 클래스 네이밍 수정 * feat: 가로모드, 다크모드 설정 (#305) * refactor: api변경에 따른 리팩토링 (#310) * feat: 로그인 화면 해상도 대응 (#313) * feat: 이미지 업로드 중일 때 로딩 상태 설정 (#317) * feat: 공모 글 작성 ui state 구현 * feat: 로딩 progressbar 생성 * feat: UI 상태에 따른 토스트 메시지 처리 * refactor: 잘못된 입력에 대한 에러 처리 변경 * refactor: 홈화면 리팩토링 (#324) * refactor: textSize dp로 변경 * refactor: 검색 버튼 크기 변경 - 검색 버튼 패딩 추가 - 검색창 끝에 패딩 추가 * refactor: 엔터키를 통해 검색하도록 수정 * refactor: 필터 단일 선택되도록 수정 * style: lint적용 * feat: 댓글방 새로운 기능 GA 연결 (#328) * feat: 댓글방 참여자 확인 Event 구현 * feat: 댓글방 상태 변경 다이얼로그 취소 Event * feat: 참여자가 공구에서 참여 포기 Event 구현 * style: ktlint 적용 * test: 테스트 데이터 수정 (#330) * feat: Fragment GA 모니터링 수집 (#332) * feat: fragment logScreenView 추적 함수 구현 * feat: 각 fragment에서 화면 감지 GA 설정 * feat: 마이페이지 기본 세팅 및 뷰 변경 (#335) * feat: 공모 참여 취소 기능 구현 (#318) * test: 공모 참여 취소 테스트코드 작성 * feat: 공모 참여 취소 기능 구현 * refactor: 불필요한 쿼리 메서드 제거 * style: 불필요한 개행 제거 * refactor: 모집중인 상태가 아닌 경우 공모 참여를 취소할 수 없도록 변경 * refactor: 공모 참여 취소 응답 상태 코드 변경 * refactor: 에러 메시지 명확한 문구로 변경 * refactor: query parameter를 적용해 어떤 공모의 참여를 취소할 것인지 의도를 명확하게 전달하도록 변경 * refactor: 총대 검증 메서드 네이밍 명확하게 변경 * feat: 댓글방 생성 시점 변경 (#319) * feat: 댓글방 생성 시점 변경 * refactor: 불필요한 도메인 OfferingWithRole 제거 * refactor: 불필요한 도메인 CommentWithRole 제거 * refactor: 댓글의 작성자 확인 메서드 추가 * refactor: 댓글방 목록 조회 dto 생성자 추가 * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 (#322) * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 * refactor: 로그인용 dto 분리 및 공통 dto에 prefix로 auth 추가 * feat: valid 어노테이션 추가 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 (#323) * refactor: 메서드명 구체적으로 변경 * refactor: 변수명 구체적으로 변경 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 * docs: todo 추가 * refactor: 함수명 통일 * feat: 공모자 여부 필드명 변경 * feat: 댓글방 상태 조회 API 확장 (#325) * feat: 댓글방 상태 조회 API 확장 * refactor: 댓글방 관련 로직 댓글 도메인으로 이동 * feat: LoggingFilter에서 던지는 유효하지 않은 요청에 대한 예외 처리 * refactor: 댓글 관련 엔드포인트 수정 * feat: 댓글방 정보 조회 시 조회 권한을 가진 사용자인지 검증 * refactor: 댓글방 상태 확인 로직 도메인으로 이동 * feat: 상태 변경을 시도하는 사용자가 총대인지 검증 * refactor: 댓글 목록 조회 엔드포인트 수정 * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 (#327) * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 * refactor: Response depth 줄이기 및 DTO 생성자 작성 * fix: imminent 필터 버그 해결 (#337) * fix: 커스텀 필터로 인해 h2-console 접속 깨지는 이슈 해결 (#339) * feat: 마이페이지 기능 구현 (#341) * feat: 마이페이지 닉네임 기능 구현 * feat: 로그아웃 로직 구현 * feat: url 연결 로직 구현 * feat: 필요없는 기능 삭제 * style: ktlint 적용 * feat: 공모 테이블에 할인율과 상태 필드 추가 (#342) * refactor: Condition과 Status 이름 변경 * refactor: 사용하지 않는 DTO 제거 * feat: OfferingEntity에 칼럼 추가 * feat: 공모 거래 날짜 필드 이름 변경 (#348) * fix: 상세화면에서 홈화면으로 갔을 때 상태 변경 안되는 오류 수정 (#343) * refactor: 공모상세페이지 Activity -> Fragment로 리팩토링 * fix: 페이지네이션 및 상태변경 미적용 오류 해결 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 주석 제거 및 상수화 * refactor: livedata 자료형 변경 * refactor: progressbar위치 수정 * refactor: lifecycleScope사용 리팩토링 * refactor: adapter에서 전체 아이템이 아닌 특정 아이템만 notify하도록 리팩토링 * refactor: API변경에 따른 대응 (#352) * refactor: api대응 * refactor: api변경에 따른 테스트 수정 * feat: 공모글 작성 화면 ux 개선 (#344) * fix: 각 항목의 설명을 place holder로 이동 * fix: 필수와 선택 항목의 프래그먼트 분리 * feat: 버튼이 항상 보이도록 수정 * fix: 가격과 총원은 숫자만 입력받도록 변경 * fix: 패딩 수정 * fix: ui 수정 * fix: 도메인 변경에 따른 deadline -> tradeDate 수정 * feat: 필수 항목을 모두 입력하면 선택 항목 화면으로 이동하는 기능 구현 * refactor: ktFormat 적용 * refactor: shared viewModel 사용, 미필수 항목을 미필수 입력 화면으로 이동 * refactor: 프래그먼트 이름 변경 * feat: 입력 숫자의 글자수와 라인수 제한 기능 구현 * fix: 총원이 -1이하로 떨어지는 버그 수정, 공동구매 텍스트 띄어쓰기 제거 * fix: 할인율, 엔빵 금액이 유효하지 않을 때는 "-"로 뜨도록 변경 * fix: 공모를 게시하면 필수, 선택 화면 모두 종료되도록 수정 * fix: 날짜 시간 픽커를 날짜만 선택하는 픽커로 변경 * refactor: ktFormat 적용 * refactor: 바인딩어댑터의 파라미터를 nullable하게 수정 * test: 테스트코드 수정 * feat: 낱개 가격의 place holder로 현재 엔빵 금액을 보여주는 기능 구현 * feat: 내용의 최대 글자수와 현재 글자수를 보여주는 기능 구현 * refactor: ktFormat 적용 * refactor: 공모글 작성시 memberId를 보내지 않도록 변경 * fix: 총원 최대 4자리에서 3자리까지만 입력받을 수 있도록 변경 * fix: deadline -> meetingDate 네이밍 수정 * fix: 공모글 작성 후 작성 화면의 입력값이 초기화되지 않는 버그 수정 * refactor: 네이밍 수정(eachPrice -> originPrice) * refactor: 네이밍 수정(individualPrice -> originPrice) * fix: 내용의 현재 글자수 색이 메인컬러가 되지 않는 문제 수정 * refactor: 프래그먼트 종료될 때 바인딩 해제하도록 수정 * refactor: id가 없는 뷰의 id 추가 * refactor: 함수 분리 * fix: 내용 옆의 * 제거 * fix: GA 이벤트 이름 변경(공모글 작성 - 필수 화면에서의 이벤트임을 명시함) * refactor: og 태그 추출 기능 수정 (#349) * refactor: crawler 패키지 이동 * feat: naver api 클라이언트 추가 refactor: 사용하지 않은 기존 og image 크롤러 명칭 변경 * feat: html 크롤링 방식과 naver api 방식을 조합하는 Extractor 구현 * fix: OfferingService ProductImageExtractor 추상화 * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 (#358) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 * test: 테스트코드 수정 * refactor: 공모글 목록 조회 필터링 수정 및 추가 (#356) * refactor: 마감임박순 필터링 이름 마감임박만으로 변경 Co-authored-by: fromitive * refactor: 필터링 쿼리 수정 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 구현 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 연결 Co-authored-by: fromitive * fix: 쿼리 내 불필요한 파라미터 제거 Co-authored-by: fromitive * refactor: 할인율이 null일 경우 높은할인율 필터링 대상에서 제외 Co-authored-by: fromitive * feat: 참여가능만 필터링 전략 클래스 추가 * feat: 공모 목록 조회 API 응답값 변경 * fix: 높은 할인율 단위 변경 및 last-id 필터링 로직 수정 * style: 주석 제거 --------- Co-authored-by: fromitive * refactor: 할인율 계산 로직 수정 (#359) * refactor: 할인율 계산 로직 수정 Co-authored-by: fromitive * refactor: 소수점 둘째 자리에서 반올림하도록 변경 Co-authored-by: fromitive * test: 할인율 계산 로직 * fix: 할인율 단위 백분율로 수정 --------- Co-authored-by: fromitive * feat: 총 모집 인원 수 최댓값 설정 (#361) Co-authored-by: fromitive * fix: 필터 오류 수정 (#362) * fix: 필터 오류 수정 - '참여가능만'필터 분기처리 제거 * chore: 주석 제거 * feat: API 스펙 변경에 따른 대응 (#364) * feat: 댓글 목록 조회 api 스펙 변경에 따른 대응 * feat: 댓글방 정보 조회 api 스펙 변경에 따른 대응 * feat: 공모 일정 조회 api 스펙 변경에 따른 대응 * feat: 댓글 상태 변경 api 스펙 변경에 따른 대응 * test: api 스펙 변경에 따른 test 코드 변경 * style: ktlint 적용 * feat: remote dto package 분리 * feat: 자동 확정 기능을 위해 스케줄러 적용 (#363) * chore: todo 추가 및 메서드명 변경 * feat: Scheduled 어노테이션 추가 및 Scheduler 분리 * test: ServiceTest 환경 구축 * feat: offeringStatus 변경 로직 추가 * refactor: 수동 확정 로직 추가 및 코드 스타일 수정 * refactor: 자동 확정 로직을 조회에서 Scheduled로 이동 * fix: 마감임박 설정 기준 내일로 변경 --------- Co-authored-by: Choo Co-authored-by: SCY * fix: 공모 작성 후 홈화면 돌아올 때 새로 작성한 글이 보이지 않는 오류 수정 (#369) * feat: Access Token, Refresh Token을 data store에 저장하는 기능 구현 (#372) * feat: 앱 재시작 시 토큰을 데이터스토어에서 꺼내 사용하는 기능 구현 * feat: 로그인이 이미 되어있다면 로그인 화면을 건너뛰는 기능 구현 * feat: 로그아웃 기능 구현 * fix: 마이페이지 화면으로 넘어가면 바텀네비게이션이 사라지는 버그 수정 * fix: 데이터스토어에서 토큰이 꺼내지지 않는 버그 수정 data store에서 토큰을 꺼내는 코루틴 비동기 작업이 끝나기 전에 함수를 종료해 버려서 생기는 버그였습니다. * refactor: ktFormat 적용 * refactor: startActivity 함수를 LoginActivity가 동반객체로 갖고 있도록 변경 * refactor: 함수명과 event명 변경 추가로 GA위치가 조금 잘못된 점이 있어서 수정했습니다. * feat: 공모 상세 화면 추가 기능 반영 (#375) * feat: 신고하기 기능 구현 * feat: 물품 링크가 없으면 보여지지 않도록 구현 * refactor: 마감 시간에서 거래 날짜로 리팩토링 * feat: 이미 참여한 공모게시글에서 채팅방으로 이동하는 기능 구현 * fix: 댓글방 목록의 마지막 댓글방이 보이지 않는 문제 수정 (#376) * fix: 리사이클러뷰 레이아웃의 크기가 화면 밖에 벗어나지 않도록 수정 * fix: 리사이클러뷰 레이아웃의 맨 밑에 구분선 하나 추가 아래로 땡겼을 때 구분선이 사라져버리는게 보기 안좋아서 추가했습니다 * refactor: 코트 포맷 적용 (컨트롤 알트 L) * feat: isManualConfirmed 제거 및 도메인 로직 확인 (#377) * refactor: isManualConfirmed 칼럼 삭제 및 관련 로직 분리 * refactor: 더미 데이터 수정 --------- Co-authored-by: fromitive * feat: API 별 권한 확인 로직 추가 (#371) * feat: 권한 확인 로직 추가 * feat: 인증 필터 적용 * refactor: 더미 데이터 칼럼 위치 변경 (#382) * refactor: 홈화면 api필드 추가에 따른 대응 (#381) * refactor: dto필드 추가 * fix: 상태 변경 오류 해결 * fix: 필터 선택 또는 검색상태일 때 공모 작성 후 나오면 목록 안보이는 오류 수정 * refactor: 세부 주소 api에서 받아오도록 변경 * style: lint적용 * fix: API 문서에 접근할 수 없는 현상 해결 (#384) * fix: API 문서에 접근할 수 없는 현상 해결 * style: 신뢰할 수 있는 URL 개행 수정 * feat: 공모 목록에서 동을 보여주는 기능 구현 (… * hotfix: og 크롤링 정상 작동 시 http https 프로토콜 정보 제거 (#459) * feat: 도메인 추가 (#15) * feat: BaseTimeEntity 추가 Co-authored-by: Dora Choo * feat: Member Entity 추가 Co-authored-by: Dora Choo * feat: Offering Entity 추가 Co-authored-by: Dora Choo * feat: OfferingMember Entity 추가 Co-authored-by: Dora Choo * feat: Comment Entity 추가 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: 공동구매 상세 조회 기능 구현 (#18) * chore: h2 환경설정 추가 * docs: http client 추가 * refactor: entity 접미어 적용 * chore: dummy data 추가 * docs: http client 값 변경 * refactor: repository 와 domain 패키지 분리 * feat: 공동구매 상세 조회 API 구현 * refactor: entity 접미어 적용 * style: 클래스 컨벤션 적용 * chore: h2 console 설정 제거 * refactor: OfferingCondition enum값 결정로직을 enum 안으로 이동 * feat: API 문서화 적용 (#23) * chore: springdoc-openapi 의존성 추가 Co-authored-by: Dora Choo * chore: springdoc 설정 추가 Co-authored-by: Dora Choo * feat: SwaggerConfig 파일 추가 Co-authored-by: Dora Choo * feat: 공모 상세 조회 API 문서화 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * fix: 공모 상세 조희 API의 price 필드 자료형 변경 및 memberId 필드 추가 (#28) * fix: 상세조회 API 금액 필드 자료형 변경 Co-authored-by: Dora Choo * fix: memberId 추가 * 내가 쓴 글인지 아닌지 확인 위해 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * chore: 백엔드 CI 및 도커 파일 작성 (#27) * chore: actions 적용 브랜치 설정 (#30) * chore: actions 적용 브랜치 설정 * chore: path 및 ref 태그 제거 * chore: working-directory 태그 추가 * chore: Dockerfile jar 경로 수정 * feat: 공모 목록 조회 기능 구현 (#35) * feat: 공모 목록 조회 API 구현 * docs: 공모 목록 조회 API http client에 추가 * fix: 공모 상세 조회 API의 status 필드를 condition으로 명칭 변경 * feat: 공모 목록 조회 API의 isClosed 필드 이름을 isOpen으로 변경 * chore: 백엔드 CD 스크립트 작성 (#34) * chore: 백엔드 CD 스크립트 작성 * chore: 도커 백그라운드로 실행 * chore: 도커 설정 및 트리거 설정 변경 * chore: 도커 이미지 제거 로직 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 강제 제거하도록 수정 * chore: gradle 캐싱 로직 추가 (#39) * chore: gradle 캐싱 로직 추가 * chore: 이벤트 트리거 조건 수정 * feat: 공모 참여하기 기능 구현 (#40) * fix: BaseTimeEntity 적용 오류 수정 Co-authored-by: Dora Choo * feat: 참여하기 API 구현 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: 공모 상세 조회 API에 참여자 목록 필드 추가 (#42) * feat: 공모 상세 조회 API의 request에 memberId 필드 추가 (#45) * feat: 공모 참여 API의 불필요한 응답값 전부 제거 (#48) * feat: 공모 참여 API의 불필요한 반환값 제거 * chore: 자주 쓰는 h2 console enabled 설정 주석 처리 * feat: 이미 참여한 공모에 참여 못하게 예외 처리 (#51) * feat: 테스트 데이터 다양화 (#52) Co-authored-by: Dora Choo * refactor: 공모 엔티티에 currentCount 필드 추가 (#55) * feat: 댓글 작성 API 구현 (#57) * feat: 댓글방 내 공모 일정 조회 기능 구현 (#58) * feat: 댓글방 내 공모 일정 조회 기능 구현 Co-authored-by: Dora Choo * refactor: 공모 일정 조회 api 명세 변경 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * refactor: common 패키지명을 global로 변경 (#61) * feat: 댓글 목록 조회 API 구현 (#66) * feat: 댓글방 목록 조회 API 구현 (#70) * refactor: 댓글방 및 댓글 목록 조회 서비스 계층 (#78) * fix: 댓글방 목록 조회 시 가장 최근 댓글 조회 (#80) * fix: 시간순 정렬 쿼리 추가 (#83) * chore: 더미 데이터 추가 (#87) * chore: 더미 데이터 바로가기 url 수정 (#93) * refactor: 댓글 도메인 코드 리팩터링 (#96) * refactor: 로그인 멤버 변수명 변경 * refactor: JPQL 쿼리 컨벤션 및 멤버로 공모 조회 메서드명 변경 * refactor: 최근 댓글 응답 클래스명 변경 * refactor: 컨트롤러 및 서비스 API 순서 변경 * refactor: 로그인 사용자 유효성 검증 * chore: 더미 데이터 시간 변경 (#100) * feat: 전반적인 예외 처리 (#103) * feat: 예외 처리 핸들러 추가 * feat: Offering 예외 처리 코드 추가 * feat: Comment 예외 처리 코드 추가 * feat: Member 예외 처리 코드 추가 * feat: OfferingMember 예외 처리 코드 추가 * feat: Offering 예외 처리 상세 코드 추가 * feat: 에러 코드 적용 * feat: 도메인 검증 로직 * feat: DTO 검증 로직 --------- Co-authored-by: masonkimseoul * feat: swagger와 restdocs 연동 (#104) * chore: swagger ui 정적 파일 설치 및 static routing 세팅 * chore: restdocs-api-spec을 이용한 OAS 생성 * chore: swagger ui 정적 파일을 swagger-ui 디렉토리로 이동 * chore: swagger ui 정적 파일 및 static routing 세팅 제거 * chore: 생성된 OAS 파일을 Swagger 디렉터리로 복사하는 스크립트 작성 * chore: openapi3 yaml 파일 gitignore 처리 * chore: static routing 세팅 다시 추가 openapi3.yaml을 사용하기 위함 * test: RestAssured RestDocs 테스트 코드 작성 * test: 공모 목록 조회 API 문서화 * test: 공모 일정 조회 API 및 공모 참여 API 문서화 * test: 댓글 관련 API 문서화 * docs: 논의된 TODO 제거 * refactor: swagger 어노테이션 제거 * chore: 개발 API 서버 목록 설정 --------- Co-authored-by: fromitive * refactor: 에러메시지 필드명 변경 (#108) * fix: restdocs 관련 테스트 실패 이슈 해결 (#106) * chore: cicd 테스트 * chore: 테스트 위해 actions 범위 조정 * chore: 배포 스크립트 띄어쓰기 오타 수정 * chore: 빌드 캐싱 제거 * chore: logging * chore: --warning-mode all 옵션 줘서 gradle 호환 무시하도록 설정 * fix: status 달라서 실패하는 테스트 수정 * chore: actions 범위 수정 * chore: action 범위 수정 * chore: test용 static 파일 추가 * chore: static 하위 폴더를 jar 파일에 포함하도록 설정 * chore: swagger-ui 하위 폴더 제거 * chore: task 순서 조정 * chore: build 스크립트 수정 * chore: 불필요한 설정 변경 제거 * chore: clean build 대신 clean bootJar 사용 * chore: clean, build 각각 하도록 변경 * chore: test 까지 두 번 돌리도록 수정 * chore: openapi3까지 두 번 실행하도록 수정 * chore: copyOasToSwagger 까지 두번 실행하도록 수정 * chore: actions 활성화 범위 수정 * fix: 댓글방 목록 조회 시 참여자 수 조건 추가 (#111) * fix: 댓글방 조회 테스트 수정 (#113) * fix: 이미지 링크 임시 수정 (#119) * fix: 이미지 링크 수정 (#120) * chore: CI 빌드 스크립트 중 중복되는 task 제거해 성능 개선 (#128) * chore: jar태스크 비활성화하고 bootJar 태스크로만 JAR 파일 생성 * chore: cicd 범위 조정 * feat: 공모 작성 API 구현 (#139) * feat: 공모 작성 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: create를 save로 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: dto entity 매핑로직을 dto로 이동 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: controller request 매개변수 명 컨벤션 적용 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 공모에 저장하는 주소 값 구체화 (#141) * refactor: 공모에 저장하는 주소 값 구체화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: github-action 스크립트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: CI/CD test 설정 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: static/swagger-ui 폴더 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: 설정 원상 복구 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: ci/cd 범위 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 댓글방 메시지 조회 시 commentId 필드 추가 (#150) Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: OG 태그 크롤링 API 구현 (#148) * feat: OG 태그 크롤링 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: OG 태그 크롤링 API 엔드포인트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 제품 코드와 API 문서 동기화 (#153) * refactor: API 문서 개선 (#157) * refactor: 댓글 작성 시 성공 상태 코드 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 요청 필수 상태 설명 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: s3 이미지 업로드 API 구현 (#147) * feat: s3 이미지 업로드 API 구현 * chore: cicd 액션 범위 수정 * fix: 이미지 업로드 경로의 특수문자 제거 * chore: yml multipart 설정 추가 * chore: S3 업로드 결과 테스트 * fix: inputstream 변환로직 위치 이동 * fix: 업로드할 s3 path 올바르게 수정 * fix: 사진 url 속에 버킷이름을 cloudfront 도메인으로 수정 * chore: actions 범위 재조정 * feat: API endpoint 변경 * chore: docker image 지우는 작업을 마지막으로 이동 * chore: 다른 브랜치로 이전 커밋 이동하기 위해 제거 * chore: 충돌 해결 및 코드 스타일 변경 * test: S3 이미지 업로드 성공 케이스 추가 * test: multipart form data 문서화 * test: 공모 상태 enum 문서화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 파일 업로드 크기 제한 100MB에서 20MB로 변경 --------- Co-authored-by: Choo Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * fix: og 태그 추출 시 크롤링 이슈 해결 (#174) * feat: 공모 목록 조회 API에 필터링과 검색 기능 추가 (#169) * feat: 공모 필터 목록 조회 API 구현 * test: 공모 필터 목록 조회 API 테스트 * style: 개행 형식 통일 * feat: 공모 필터 목록 조회 API Specification 도입 준비 * fix: url에 큰따움표 제거 * feat: Specification 도입 * refactor: queryString 구체화 * refactor: 함수명 변경 * feat: 최신순 필터링 적용 * feat: 마감임박순 필터링 적용 * feat: 높은할인률순 필터링 적용 * refactor: 전략 패턴 적용해 여러 갈래의 분기문과 중복되는 코드 처리 * test: 변경된 API 스펙에 맞게 문서화 작업 * refactor: 관련있는 메서드들끼리 모이게 순서 재배치 * refactor: 맞춤법 수정 * style: 개행 제거 --------- Co-authored-by: masonkimseoul * feat: 상태 변경 API 구현 (#175) * feat: 댓글방 상태 변경 및 조회 API 구현 Co-authored-by: masonkimseoul * feat: 공모글 상태 조회 API 구현 * feat: 댓글방 상태 변경 중 수동 확정 기능 구현 * refactor: 상태 변경 관련 메서드명 수정 * refactor: 추상 클래스 메서드 컨벤션 통일 * refactor: errorCode 사용 시 클래스 명시 * refactor: 댓글방 상태 관련 API 엔드포인트 수정 및 패키지 변경 * refactor: 댓글방 상태 변경 API HTTP 메서드 수정 * feat: 공모 모집 자동 확정 시 댓글방 상태 변경 --------- Co-authored-by: masonkimseoul Co-authored-by: Choo * feat: 로그인 기능 구현 (#177) * feat: password 일방향 암호화 기능 구현 * feat: cookie 생산-소비 기능 구현 * chore: jwt 관련 의존성 추가 * feat: 토큰 생성 기능 구현 * feat: 로그인 API 구현 * test: 로그인 API 테스트 * feat: 회원가입 API 구현 * test: 회원가입 API 테스트 * feat: 닉네임 생성 기능 구현 * test: 닉네임 생성 기능 테스트 * fix: postconstruct 여러 개라 발생한 에러 해결 * feat: 회원가입 응답값에 랜덤생성한 닉네임 추가 * feat: MemberArgumentResolver 구현 * feat: MemberArgumentResolver 일부 적용 * test: 바뀐 스펙에 맞게 변경 * test: TestConfig 설정해 빈충돌 오류 해결 * test: 공모 작성 API로 MemberArgumentResolver 사용 * feat: 토큰 재발급 API 구현 * test: 토큰 재발급 API 테스트 * test: 토큰 재발급 API 에러 테스트 * feat: MemberArgumentResolver commant에 적용 * feat: MemberArgumentResolver offering에 적용 * feat: MemberArgumentResolver participant에 적용 * refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경 * refactor: 클래스명 일관적으로 변경 * refactor: 직관적인 명명으로 enum 네이밍 변경 * refactor: Custom Exception 적용 * refactor: 컨트롤러 메서드에 접근제어자 명시 * fix: 중복된 enum 값 제거 * test: 바뀐 API 스펙에 맞게 변경 --------- Co-authored-by: fromitive * fix: nicknameWordInitializer 설정 오류 해결 (#182) * fix: keyword null일 때 처리 및 docs에서 required 제거 (#184) * fix: keyword null일 때 처리 * test: optional() 붙여서 required 제거 * chore: 브랜치에 상관없이 pr 머지 시 자동으로 관련 이슈 닫는 스크립트 구현 (#187) * fix: og 이미지 태그 크롤링 문제 해결 (#190) * refactor: 댓글방 상태 도메인 설계 변경 (#189) * feat: 공모 목록 API 응답값에 낱개 가격 추가 (#193) * chore: readtimeout 5초로 수정 (#195) * feat: 댓글방 상태 조회 시 상태별 이미지 함께 반환 (#196) * feat: 토큰 반환 시 cookie가 아닌 body 사용하도록 변경 (#206) * feat: 발급한 토큰을 header가 아닌 body로 반환하도록 수정 * refactor: 사용안하는 클래스와 메서드 제거 * test: 바뀐 API 스펙에 맞게 명세 수정 * feat: 이미지 더미 데이터 수정 및 부정확한 가격 데이터 수정 (#207) * refactor: 공모 글 작성 시 총대 참여자 추가 (#208) * feat: 데이터에서 5자 이상 제거 (#212) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 (#202) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 * refactor: 도메인 명칭 변경 (낱개가격 -> 원가격) * refactor: 도메인 명칭 변경 (공모 -> 댓글방) * refactor: originPrice로 http client 변경 * feat: 토큰 반환 시 body가 아닌 cookie로 반환하도록 원상복구 (#223) * feat: 토큰 재발급 API에서 requestHeader로 refreshToken 받도록 수정 (#227) * feat: 토큰 재발급 API에서 body가 아닌 cookie로 토큰 반환 * feat: 회원가입 API도 body가 아닌 cookie로 토큰 반환 * refactor: service 용 dto 명 컨벤션에 맞춰 수정 * feat: 댓글방 일정 수정 API 구현 (#226) * feat: 댓글방 일정 수정 API 구현 * test: 총대가 아닌 참여자가 공모 일정 정보를 수정할 경우 예외 발생 * feat: 댓글방 상태 조회 시 버튼 텍스트 추가 (#229) * feat: 공모 참여자 목록 조회 API 구현 (#225) * feat: 공모 참여자 목록 조회 API 구현 * test: 실패 테스트 오류 수정 * style: 띄어쓰기 적용 * refactor: MemberEntity를 받도록 변경 * refactor: isParticipant를 구현하여 가독성 개선 * refactor: 총대를 찾을 수 없는 상황의 예외 추가 * refactor: 참여 검증로직을 서비스로 이동 * refactor: 사용하지 않는 메서드 제거 * refactor: 검증 로직 가장 상단에 위치 * refactor: 총대 추출 로직 수정 --------- Co-authored-by: masonkimseoul Co-authored-by: SCY * refactor: 마감임박순 필터링 쿼리 조건 수정 (#239) * refactor: 마감임박순 필터링 조건 수정 * refactor: 더미 데이터 시간 수정 * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 (#247) * feat: 로깅 코드 삽입 (#266) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 * feature: 로깅 샘플 구현 * refactor: 불필요한 코드 제거 * feat: logging 적용 --------- Co-authored-by: fromitive * fix: 마감 임박 필터링 쿼리 수정 (#267) * chore: logback 설정 진행 (#270) * chore: logback 설정 * fix: multipart 요청 필터링 * chore: logback 설정 변경 * chore: pull request ci/cd 닫기 * fix: 이미지 업로드 API의 responseBody가 두 번 뜨는 오류 해결 (#273) * fix: 이미지 업로드 API 두 번 도는 문제 해결 * test: 이미지 업로드 API의 누락된 response field 추가 * feat: pageSize validation 추가 (#279) * feat: pageSize validation 추가 * feat: magic number 추출 * test: 테스트 데이터 수정 (#330) * feat: 공모 참여 취소 기능 구현 (#318) * test: 공모 참여 취소 테스트코드 작성 * feat: 공모 참여 취소 기능 구현 * refactor: 불필요한 쿼리 메서드 제거 * style: 불필요한 개행 제거 * refactor: 모집중인 상태가 아닌 경우 공모 참여를 취소할 수 없도록 변경 * refactor: 공모 참여 취소 응답 상태 코드 변경 * refactor: 에러 메시지 명확한 문구로 변경 * refactor: query parameter를 적용해 어떤 공모의 참여를 취소할 것인지 의도를 명확하게 전달하도록 변경 * refactor: 총대 검증 메서드 네이밍 명확하게 변경 * feat: 댓글방 생성 시점 변경 (#319) * feat: 댓글방 생성 시점 변경 * refactor: 불필요한 도메인 OfferingWithRole 제거 * refactor: 불필요한 도메인 CommentWithRole 제거 * refactor: 댓글의 작성자 확인 메서드 추가 * refactor: 댓글방 목록 조회 dto 생성자 추가 * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 (#322) * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 * refactor: 로그인용 dto 분리 및 공통 dto에 prefix로 auth 추가 * feat: valid 어노테이션 추가 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 (#323) * refactor: 메서드명 구체적으로 변경 * refactor: 변수명 구체적으로 변경 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 * docs: todo 추가 * refactor: 함수명 통일 * feat: 공모자 여부 필드명 변경 * feat: 댓글방 상태 조회 API 확장 (#325) * feat: 댓글방 상태 조회 API 확장 * refactor: 댓글방 관련 로직 댓글 도메인으로 이동 * feat: LoggingFilter에서 던지는 유효하지 않은 요청에 대한 예외 처리 * refactor: 댓글 관련 엔드포인트 수정 * feat: 댓글방 정보 조회 시 조회 권한을 가진 사용자인지 검증 * refactor: 댓글방 상태 확인 로직 도메인으로 이동 * feat: 상태 변경을 시도하는 사용자가 총대인지 검증 * refactor: 댓글 목록 조회 엔드포인트 수정 * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 (#327) * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 * refactor: Response depth 줄이기 및 DTO 생성자 작성 * fix: imminent 필터 버그 해결 (#337) * fix: 커스텀 필터로 인해 h2-console 접속 깨지는 이슈 해결 (#339) * feat: 공모 테이블에 할인율과 상태 필드 추가 (#342) * refactor: Condition과 Status 이름 변경 * refactor: 사용하지 않는 DTO 제거 * feat: OfferingEntity에 칼럼 추가 * feat: 공모 거래 날짜 필드 이름 변경 (#348) * refactor: og 태그 추출 기능 수정 (#349) * refactor: crawler 패키지 이동 * feat: naver api 클라이언트 추가 refactor: 사용하지 않은 기존 og image 크롤러 명칭 변경 * feat: html 크롤링 방식과 naver api 방식을 조합하는 Extractor 구현 * fix: OfferingService ProductImageExtractor 추상화 * refactor: 공모글 목록 조회 필터링 수정 및 추가 (#356) * refactor: 마감임박순 필터링 이름 마감임박만으로 변경 Co-authored-by: fromitive * refactor: 필터링 쿼리 수정 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 구현 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 연결 Co-authored-by: fromitive * fix: 쿼리 내 불필요한 파라미터 제거 Co-authored-by: fromitive * refactor: 할인율이 null일 경우 높은할인율 필터링 대상에서 제외 Co-authored-by: fromitive * feat: 참여가능만 필터링 전략 클래스 추가 * feat: 공모 목록 조회 API 응답값 변경 * fix: 높은 할인율 단위 변경 및 last-id 필터링 로직 수정 * style: 주석 제거 --------- Co-authored-by: fromitive * refactor: 할인율 계산 로직 수정 (#359) * refactor: 할인율 계산 로직 수정 Co-authored-by: fromitive * refactor: 소수점 둘째 자리에서 반올림하도록 변경 Co-authored-by: fromitive * test: 할인율 계산 로직 * fix: 할인율 단위 백분율로 수정 --------- Co-authored-by: fromitive * feat: 총 모집 인원 수 최댓값 설정 (#361) Co-authored-by: fromitive * feat: 자동 확정 기능을 위해 스케줄러 적용 (#363) * chore: todo 추가 및 메서드명 변경 * feat: Scheduled 어노테이션 추가 및 Scheduler 분리 * test: ServiceTest 환경 구축 * feat: offeringStatus 변경 로직 추가 * refactor: 수동 확정 로직 추가 및 코드 스타일 수정 * refactor: 자동 확정 로직을 조회에서 Scheduled로 이동 * fix: 마감임박 설정 기준 내일로 변경 --------- Co-authored-by: Choo Co-authored-by: SCY * feat: isManualConfirmed 제거 및 도메인 로직 확인 (#377) * refactor: isManualConfirmed 칼럼 삭제 및 관련 로직 분리 * refactor: 더미 데이터 수정 --------- Co-authored-by: fromitive * feat: API 별 권한 확인 로직 추가 (#371) * feat: 권한 확인 로직 추가 * feat: 인증 필터 적용 * refactor: 더미 데이터 칼럼 위치 변경 (#382) * fix: API 문서에 접근할 수 없는 현상 해결 (#384) * fix: API 문서에 접근할 수 없는 현상 해결 * style: 신뢰할 수 있는 URL 개행 수정 * feat: 공모 단건 조회 API 구현 (#388) * feat: 공모 상세 조회 API 엔드포인트 변경 * feat: 공모 단건 조회 API * style: 공모 관련 API 순서 변경 * test: 불필요한 공모글 생성 코드 제거 * test: 공모 단건 조회 서비스 테스트 * chore: JAR 파일에 OAS 파일 누락되는 이슈 해결 및 중복 task 제거 (#391) * chore: 중복되는 task 제거 * chore: cicd 범위 조정 * fix: 참여자 목록 조회 API에서 totalCount 반환하지 않는 이슈 해결 (#400) * fix: /auth/refresh endpoint accessToken 검증 예외 추가 (#407) * refactor: 더미 데이터 정합성 확보 (#406) * refactor: 더미 데이터 정합성 확보 * refactor: 추가된 칼럼 반영 * chore: 환경에 따른 yml 파일 분리 (#411) * chore: 환경 별로 yml 파일 분리 * chore: 불필요한 yml 설정 제거 * feat: 카카오 로그인 중 사용자 정보 확인 로직을 안드로이드에서 백엔드로 이관 (#404) * feat: 카카오 로그인 API 구현 * feat: providerId를 loginId로 수정 * feat: 소셜 로그인 시 랜덤 생성된 비밀번호 사용 * refactor: 불필요한 api 제거 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY * test: 로그인 로직 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY * test: MemberFixture 불필요한 함수 제거 및 통일 Co-authored-by: fromitive Co-authored-by: Dora Choo * refactor: 불필요한 정보 제거 Co-authored-by: fromitive Co-authored-by: Dora Choo * feat: 카카오 로그인 에러 핸들러 추가 Co-authored-by: fromitive Co-authored-by: Dora Choo * feat: 민감 정보 로깅에서 제외 Co-authored-by: fromitive Co-authored-by: Dora Choo --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY Co-authored-by: fromitive * feat: cookie 관련 예외 처리 (#409) * refactor: 더미 데이터 http 추가 (#422) * fix: 더미데이터 정합성 맞추기 (#425) * feat: 로깅 시 UUID가 아닌 회원 번호가 기록되도록 변경 (#428) * feat: logging 시 memberId가 나오도록 기능 추가 * feat: logging 시 memberId 및 identifier가 함께 나오도록 변경 * refactor: lombok getter 적용 * feat: Spring Timezone KST로 설정 (#430) * chore: Dockerfile 타임존 변경 (#432) * fix: Offering 목록 조회 시 NPE 해결 (#434) * refactor: 외래키 필드 notnull 조건 추가 (#445) * chore: prod CI/CD 구축 (#423) * chore: 환경 별로 yml 파일 분리 * chore: 운영 서버 CI/CD 스크립트 작성 * chore: 운영 환경 내 swagger 문서 제거 * chore: 운영 환경 포트포워딩 명령어 제거 * chore: prod ci/cd 스크립트 트리거 추가 * chore: prod ci/cd 스크립트 트리거 변경 * chore: prod ci/cd 스크립트 트리거 path 구체화 * chore: prod ci/cd 스크립트 docker 실행 명령어 오타 수정 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev 및 prod ci/cd 스크립트 data.sql 실행 비활성화 * chore: prod ci/cd 스크립트 path 롤백 * chore: dev script test --------- Co-authored-by: Choo * chore: prod 불필요한 트리거 주석 처리 (#447) * v1.1.0 (#448) * feat: 게시글 상세 화면 구현 (#8) * feat: 게시글 상세 화면 레이아웃 작성 * feat: Data layer코드 작성 * refactor: dto패키지 분리, dto에 serialName추가 * refactor: 도메인 모델 수정 - 가변에서 불변으로 변경 - 사용하지 않는 메서드 제거 * refactor: 공통으로 사용되거나 사용될 수 있는 확장함수를 별도의 파일로 분리 * style: lint 적용 * refactor: 메서드명 컨벤션 적용 * refactor: request Dto에 SerialName적용 * refactor: 메서드명 수정 * feat: 도메인 추가 (#15) * feat: BaseTimeEntity 추가 Co-authored-by: Dora Choo * feat: Member Entity 추가 Co-authored-by: Dora Choo * feat: Offering Entity 추가 Co-authored-by: Dora Choo * feat: OfferingMember Entity 추가 Co-authored-by: Dora Choo * feat: Comment Entity 추가 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: BottomNavigation 구현 (#16) * chore: jetpack navigation 라이브러리 추가 * feat: 필요한 바텀 네비게이션 리소스 추가 * feat: bottom navigation fragment 추가 * feat: bottom navigation graph 구현 * refactor: 컨벤션에 맞게 id 수정 * feat: 공동구매 상세 조회 기능 구현 (#18) * chore: h2 환경설정 추가 * docs: http client 추가 * refactor: entity 접미어 적용 * chore: dummy data 추가 * docs: http client 값 변경 * refactor: repository 와 domain 패키지 분리 * feat: 공동구매 상세 조회 API 구현 * refactor: entity 접미어 적용 * style: 클래스 컨벤션 적용 * chore: h2 console 설정 제거 * refactor: OfferingCondition enum값 결정로직을 enum 안으로 이동 * feat: 홈화면, 마이페이지 화면 레이아웃 작성 (#19) * refactor: FragmentContainer width 속성 수정 * feat: 홈 화면 레이아웃 작성 * feat: 마이페이지 화면 레이아웃 작성 * fix: 플로팅 버튼이 홈에서만 보이도록 수정 * refactor: 리소스 네이밍 컨벤션에 맞게 수정 * feat: API 문서화 적용 (#23) * chore: springdoc-openapi 의존성 추가 Co-authored-by: Dora Choo * chore: springdoc 설정 추가 Co-authored-by: Dora Choo * feat: SwaggerConfig 파일 추가 Co-authored-by: Dora Choo * feat: 공모 상세 조회 API 문서화 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * fix: 공모 상세 조희 API의 price 필드 자료형 변경 및 memberId 필드 추가 (#28) * fix: 상세조회 API 금액 필드 자료형 변경 Co-authored-by: Dora Choo * fix: memberId 추가 * 내가 쓴 글인지 아닌지 확인 위해 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * chore: 백엔드 CI 및 도커 파일 작성 (#27) * chore: actions 적용 브랜치 설정 (#30) * chore: actions 적용 브랜치 설정 * chore: path 및 ref 태그 제거 * chore: working-directory 태그 추가 * chore: Dockerfile jar 경로 수정 * feat: 댓글방 목록 구현 (#26) * feat: 댓글방 목록 UI 구현 * fix: 구분선을 ImageView에서 View로 변경 * feat: 댓글방 목록 도메인 모델 구현 * feat: 댓글방 어답터 구현 * feat: "채팅" string 추가 * refactor: 불필요한 코드 제거 * fix: xmls 중복 속성 제거 * refactor: 댓글방 클래스들을 comment 패키지로 분리 * refactor: 컬러와 폰트 사이즈를 values 파일로 분리 * feat: 공모 목록 조회 기능 구현 (#35) * feat: 공모 목록 조회 API 구현 * docs: 공모 목록 조회 API http client에 추가 * fix: 공모 상세 조회 API의 status 필드를 condition으로 명칭 변경 * feat: 공모 목록 조회 API의 isClosed 필드 이름을 isOpen으로 변경 * feat: 댓글방 디테일 화면 구현 (#32) * feat: font 설정 * feat: vector 이미지 추가 * feat: 채팅 아이템 뷰 구현 * refactor: 컨벤션에 맞게 네이밍 수정 * feat: 댓글 입력 edit text 구현 * chore: 백엔드 CD 스크립트 작성 (#34) * chore: 백엔드 CD 스크립트 작성 * chore: 도커 백그라운드로 실행 * chore: 도커 설정 및 트리거 설정 변경 * chore: 도커 이미지 제거 로직 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 제거 방식 수정 * chore: 도커 이미지 강제 제거하도록 수정 * chore: gradle 캐싱 로직 추가 (#39) * chore: gradle 캐싱 로직 추가 * chore: 이벤트 트리거 조건 수정 * feat: 공모 참여하기 기능 구현 (#40) * fix: BaseTimeEntity 적용 오류 수정 Co-authored-by: Dora Choo * feat: 참여하기 API 구현 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * feat: 공모 상세 조회 API에 참여자 목록 필드 추가 (#42) * feat: 공모 상세 조회 API의 request에 memberId 필드 추가 (#45) * feat: 공모 참여 API의 불필요한 응답값 전부 제거 (#48) * feat: 공모 참여 API의 불필요한 반환값 제거 * chore: 자주 쓰는 h2 console enabled 설정 주석 처리 * feat: 이미 참여한 공모에 참여 못하게 예외 처리 (#51) * feat: 공모 상세 페이지 API 연결 (#46) * build: 불필요한 의존성 제거, properties관련 코드 작성 * refactor: base_url코드상에서 제거 * feat: api수정에 따른 필드 변경 및 네이밍 반영 * refactor: 네이밍 변경 * refactor: OfferingDetail의 변경, mapper변경 * refactor: service분리 * refactor: DataSource, Repository분리 * refactor: API변경에 따른 리팩토링 * feat: 공모 상세 조회 기능 구현 * refactor: 참여하기 api변경에 따른 data, domain 코드 수정 * feat: 공모 상세 페이지 참여하기 기능 구현 * feat: 공모 상세 화면에서 이미지를 불러올 수 없을 시 기본이미지를 보여주는 기능 구현 * feat: 게시물 상세 화면 폰트 적용 * style: lint적용 * refactor: 액티비티 destroy시 binding해제하도록 코드 추가 * refactor: glide옵션 변경 - 에러 발생 시 보여줄 이미지 - url이 null일 시 보여줄 이미지 * refactor: viewModel에 custom getter추가 * fix: 내용이 짧을 시 뒷 배경이 회색으로 보이는 버그 수정 * fix: 참여하기 버튼을 눌렀을 시 텍스트가 바뀌지 않는 버그 수정 * feat: 테스트 데이터 다양화 (#52) Co-authored-by: Dora Choo * refactor: 공모 엔티티에 currentCount 필드 추가 (#55) * feat: 댓글 작성 API 구현 (#57) * feat: 댓글방 내 공모 일정 조회 기능 구현 (#58) * feat: 댓글방 내 공모 일정 조회 기능 구현 Co-authored-by: Dora Choo * refactor: 공모 일정 조회 api 명세 변경 Co-authored-by: Dora Choo --------- Co-authored-by: Dora Choo * refactor: common 패키지명을 global로 변경 (#61) * chore: 안드로이드 CI 파일 작성 (#63) * feat: 댓글 목록 조회 API 구현 (#66) * chore: build CI 작업을 위한 manifest 파일 수정 (#65) * chore: 알람 권한 추가 * chore: local properties 속성 추가 * chore: local properties null 체크 로직 추가 * chore: buildConfigField null 체크 * style: lint 적용 * chore: secret 값 설정 * fix: secret 값 오류 수정 * fix: 문법 오류 수정 * chore: 경로 수정 * chore: 문법 수정 * style: lint 적용 * feat: 댓글방 목록 조회 API 구현 (#70) * feat 댓글방 접히는 공지 뷰 구현 (#72) * chore: manifest에 CommentDetailActivity 추가 * feat: BindingAdatper을 사용하여 접힐 때 애니메이션 적용 및 픽셀 변환 * feat: viewmodel 구현 및 click 마다 접히고 펴지는 로직 구현 * style: ktlint 적용 * refactor: binding adpater을 사용하여 가시성 변경 * refactor: 댓글방 및 댓글 목록 조회 서비스 계층 (#78) * fix: 댓글방 목록 조회 시 가장 최근 댓글 조회 (#80) * feat: 홈화면 API 연결 (#74) * refactor: API변경에 따른 data, domain 코드 변경 * feat: 공모 목록 기능 구현 * refactor: 함수 분리 * style: lint적용 * style: font 적용 * fix: 시간순 정렬 쿼리 추가 (#83) * chore: 더미 데이터 추가 (#87) * feat: 댓글방 목록 API 연결 (#82) * feat: bottom navigation fragment 추가 * feat: vector 이미지 추가 * feat: 댓글방이 없으면 "채팅 목록이 없어요" 라는 텍스트뷰와 이미지뷰를 띄우는 기능 구현 * feat: 댓글방 띄우는 기능 구현 * test: 댓글방 UI 테스트 작성 * refactor: 테스트 클래스명 수정 * refactor: 줄바꿈 수정 * feat: 댓글방 API 서비스 구현 * refactor: API 명세에 따라 도메인 모델 수정 * feat: API 연결 * refactor: API명세에 따라 데이터바인딩 변수명 수정 * feat: 댓글방 목록 API 연결 * refactor: ktlint Format 적용 * refactor: 메모리 누수 방지를 위해 fragment가 destroy 될 때 _binding을 null로 설정 * refactor: 어답터를 방어적복사 하지 않아도 되어서 수정 * refactor: 채팅방이 없다는 이미지뷰를 띄워주는 방식 수정(바인딩 어댑터 수정) * refactor: 함수분리 * refactor: ktFormat 적용 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> * feat: 댓글방 접히는 공지 API 연결 (#85) * feat: 미팅 일정 API 연결을 위한 data layer 구현 * feat: 미팅 일정 API 연결을 위한 domain layer 구현 * feat: 미팅 일정 API 연결을 위한 presentation layer 구현 * style: ktlint 적용 * feat: 공동 구매 제목 databinding 적용 * refactor: 변수명 수정 * fix: 펼치기 접기 버튼 로직 반대로 수정 * style: ktlint 적용 * chore: 더미 데이터 바로가기 url 수정 (#93) * feat: 공모 상세 페이지 기능 추가 (#94) * chore: 마이페이지 닉네임 임시로 지정 * feat: 바로가기 기능 구현 * feat: 참여버튼 클릭 시 댓글방으로 가도록 기능 구현 * feat: 신고하기 이미지 추가 * style: lint적용 * refactor: 불러오는 공모 페이지 사이즈 변경 * refactor: 댓글 도메인 코드 리팩터링 (#96) * refactor: 로그인 멤버 변수명 변경 * refactor: JPQL 쿼리 컨벤션 및 멤버로 공모 조회 메서드명 변경 * refactor: 최근 댓글 응답 클래스명 변경 * refactor: 컨트롤러 및 서비스 API 순서 변경 * refactor: 로그인 사용자 유효성 검증 * feat: 댓글방 댓글 작성 api 연결 (#95) * chore: windowSoftInputMode 추가 * feat: post comment api service 구현 * feat: post comment DataSource 구현 * feat: post comment Repository 구현 * feat: post comment Presentation 구현 * chore: 더미 데이터 시간 변경 (#100) * feat: 댓글방 입장 기능, 본인이 총대인 방은 다르게 보이는 기능 구현 (#99) * feat: 댓글방의 마지막 댓글 시간을 띄우는 기능 구현 * feat: 자신이 총대인 댓글방을 표시하는 기능 구현 * feat: 댓글방 목록을 클릭해 댓글방 상세로 이동하는 기능 구현 * test: UI테스트 수정 * refactor: 클릭시 id 뿐만 아니라 title도 받아오는 방식으로 수정 * refactor: 오전/오후와 시간을 텍스트뷰에 띄우는 바인딩 어댑터를 DateTimeFormatter의 기능을 사용하는 것으로 수정 * refactor: memberId를 local.properties의 token을 가져다 쓰는 것으로 변경(임시 조치) * refactor: 댓글방 목록의 시간을 띄우는 바인딩 어댑터의 속성명을 수정함 * refactor: 데이터바인딩 variable 변수명을 구체적으로 수정, 일관성을 위해 앞에 `on` 붙임 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정 * refactor: 어댑터가 뷰모델을 갖고 있지 않도록 수정(빠트린것 수정함) * feat: 전반적인 예외 처리 (#103) * feat: 예외 처리 핸들러 추가 * feat: Offering 예외 처리 코드 추가 * feat: Comment 예외 처리 코드 추가 * feat: Member 예외 처리 코드 추가 * feat: OfferingMember 예외 처리 코드 추가 * feat: Offering 예외 처리 상세 코드 추가 * feat: 에러 코드 적용 * feat: 도메인 검증 로직 * feat: DTO 검증 로직 --------- Co-authored-by: masonkimseoul * feat: swagger와 restdocs 연동 (#104) * chore: swagger ui 정적 파일 설치 및 static routing 세팅 * chore: restdocs-api-spec을 이용한 OAS 생성 * chore: swagger ui 정적 파일을 swagger-ui 디렉토리로 이동 * chore: swagger ui 정적 파일 및 static routing 세팅 제거 * chore: 생성된 OAS 파일을 Swagger 디렉터리로 복사하는 스크립트 작성 * chore: openapi3 yaml 파일 gitignore 처리 * chore: static routing 세팅 다시 추가 openapi3.yaml을 사용하기 위함 * test: RestAssured RestDocs 테스트 코드 작성 * test: 공모 목록 조회 API 문서화 * test: 공모 일정 조회 API 및 공모 참여 API 문서화 * test: 댓글 관련 API 문서화 * docs: 논의된 TODO 제거 * refactor: swagger 어노테이션 제거 * chore: 개발 API 서버 목록 설정 --------- Co-authored-by: fromitive * refactor: 에러메시지 필드명 변경 (#108) * fix: restdocs 관련 테스트 실패 이슈 해결 (#106) * chore: cicd 테스트 * chore: 테스트 위해 actions 범위 조정 * chore: 배포 스크립트 띄어쓰기 오타 수정 * chore: 빌드 캐싱 제거 * chore: logging * chore: --warning-mode all 옵션 줘서 gradle 호환 무시하도록 설정 * fix: status 달라서 실패하는 테스트 수정 * chore: actions 범위 수정 * chore: action 범위 수정 * chore: test용 static 파일 추가 * chore: static 하위 폴더를 jar 파일에 포함하도록 설정 * chore: swagger-ui 하위 폴더 제거 * chore: task 순서 조정 * chore: build 스크립트 수정 * chore: 불필요한 설정 변경 제거 * chore: clean build 대신 clean bootJar 사용 * chore: clean, build 각각 하도록 변경 * chore: test 까지 두 번 돌리도록 수정 * chore: openapi3까지 두 번 실행하도록 수정 * chore: copyOasToSwagger 까지 두번 실행하도록 수정 * chore: actions 활성화 범위 수정 * fix: 댓글방 목록 조회 시 참여자 수 조건 추가 (#111) * fix: 댓글방 조회 테스트 수정 (#113) * feat: 홈 화면 무한 스크롤 기능 구현 (#109) * build: pagination라이브러리 추가 * feat: 홈 화면 무한 스크롤 기능 구현 * fix: 마지막 댓글 response를 nullable하게 수정 (#115) * fix: 마지막 댓글 response를 nullable하게 수정 * refactor: ktFormat 적용 * feat: 댓글방 댓글 조회 api 연결 (#116) * feat: dto 및 mapper 구현 * feat: 댓글방 목록 service 구현 * feat: 댓글방 목록 data source 구현 * feat: 댓글방 목록 repository 및 model 구현 * feat: 댓글방 목록 view type을 활용한 recyclerview 구현 및 데이터 바인딩 * feat: polling 기능 구현 * feat: 댓글 스크롤 구현 (새로운 댓글이 생길시 스크롤 아래로) * feat: 총대와 다른 참가자 이미지 리소스 파일 * feat: 댓글방 디테일 공동 구매 상태별 관리 (#117) * feat: 공동구매 상태 관리 리소스 파일 * feat: 공동구매 상태를 관리하는 enum class 구현 * feat: 데이터바인딩을 사용하여 공동 구매 상태 뷰 업데이트 구현 * style: ktlint 적용 * feat: 공동구매 상태 관리 리소스 파일 추가 * fix: 이미지 링크 임시 수정 (#119) * fix: 이미지 링크 수정 (#120) * refactor: 네이밍 수정 (#123) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 (#125) * refactor: 뷰모델 팩토리 방식 변경 (#130) * refactor: 뷰모델 팩토리를 뷰모델의 동반객체로 이동 * style: lint적용 * refactor: Service분리 (#132) * refactor: service분리 * refactor: 패키지명 변경 * style: lint적용 * feat: 공모글 작성 UI 구현 (#134) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 뷰 구현 * fix: 뷰 수정사항 반영 * fix: @+id로 참조하는 부분을 수정 * fix: drawable의 네이밍에 where을 추가 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 (#136) * feat: 참여자 목록 drawer에 필요한 리소스 파일 추가 * refactor: 채팅 text gravity 수정 * feat: 댓글방 참여자 목록 Drawer Layout UI 구현 * style: ktlint 적용 * refactor: drawer early return 하는 방식으로 변경 * refactor: ivMore -> ivMoreOptions으로 네이밍 변경 * feat: 공구 참여자 item view 및 댓글방 view 사용자 친화적으로 수정 * chore: CI 빌드 스크립트 중 중복되는 task 제거해 성능 개선 (#128) * chore: jar태스크 비활성화하고 bootJar 태스크로만 JAR 파일 생성 * chore: cicd 범위 조정 * feat: 공모 작성 API 구현 (#139) * feat: 공모 작성 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: create를 save로 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: dto entity 매핑로직을 dto로 이동 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: controller request 매개변수 명 컨벤션 적용 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 공모에 저장하는 주소 값 구체화 (#141) * refactor: 공모에 저장하는 주소 값 구체화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: github-action 스크립트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: CI/CD test 설정 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: static/swagger-ui 폴더 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: 설정 원상 복구 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * chore: ci/cd 범위 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 홈화면(공모목록) UI 추가 구현 및 상태 변경 대응 (#142) * feat: 공모의 상태 변경이 반영되도록 기능 구현 * feat: 공모 목록 ui변경 * feat: 필터 ui추가 * feat: API변경에 따른 DTO수정 * style: lint적용 * feat: resource추가 * refactor: ui위치 수정 * chore: 불필요한 괄호 제거 * refactor: item 수직 정렬 * feat: 댓글방 메시지 조회 시 commentId 필드 추가 (#150) Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: OG 태그 크롤링 API 구현 (#148) * feat: OG 태그 크롤링 API 구현 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: OG 태그 크롤링 API 엔드포인트 수정 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 제품 코드와 API 문서 동기화 (#153) * refactor: API 문서 개선 (#157) * refactor: 댓글 작성 시 성공 상태 코드 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * refactor: 요청 필수 상태 설명 추가 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: s3 이미지 업로드 API 구현 (#147) * feat: s3 이미지 업로드 API 구현 * chore: cicd 액션 범위 수정 * fix: 이미지 업로드 경로의 특수문자 제거 * chore: yml multipart 설정 추가 * chore: S3 업로드 결과 테스트 * fix: inputstream 변환로직 위치 이동 * fix: 업로드할 s3 path 올바르게 수정 * fix: 사진 url 속에 버킷이름을 cloudfront 도메인으로 수정 * chore: actions 범위 재조정 * feat: API endpoint 변경 * chore: docker image 지우는 작업을 마지막으로 이동 * chore: 다른 브랜치로 이전 커밋 이동하기 위해 제거 * chore: 충돌 해결 및 코드 스타일 변경 * test: S3 이미지 업로드 성공 케이스 추가 * test: multipart form data 문서화 * test: 공모 상태 enum 문서화 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 파일 업로드 크기 제한 100MB에서 20MB로 변경 --------- Co-authored-by: Choo Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> * feat: 주소검색 기능구현 (#161) * refactor: 네이밍 컨벤션 적용 * build: webview 라이브러리 추가 * feat: 스크립트 실행위한 html파일 추가 * refactor: 인터페이스명 변경에 따른 변경 * feat: 주소검색 다이얼로그 레이아웃 작성 * feat: 주소검색 기능 구현 * style: lint적용 * refactor: 불필요한 코드 제거 * build: Firebase의존성 추가 (#165) * feat: 공모글 작성 API 연결 (#162) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 공모글 작성 API 연결 구현 * feat: 공모글 작성 뷰모델 구현 * fix: edit text 데이터바인딩 추가 * chore: 테스트를 위해 MutableLiveData default값 넣어둠 * chore: deadline defualt값 형식에 맞게 수정 * feat: 글작성 화면을 액티비티에서 프래그먼트로 수정 * chore: 테스트목적이었던 주석과 mutable livedata 디폴트값 제거 * refactor: 임시 함수명 수정 * fix: 글작성 프래그먼트가 올라오기 전에 바텀 네비게이션이 사라지는 문제 수정 * feat: 필수 항목이 모두 입력되어야 버튼이 활성화 되는 기능 구현 * feat: 가격, 총원 입력이 잘못되었을 시 토스트를 띄우는 기능 구현 * fix: 버튼 비활성화 시 텍스트 변경 * feat: 앱 아이콘 변경 * feat: 앱 이름 변경(chongdae -> 총대마켓) * feat: 예상 엔빵 가격을 보여주는 기능 구현 * refactor: 상수화 * refactor: 예상 엔빵 가격에 ,가 들어가는 기능 구현, 콜론 뒤 white space 추가 * feat: 공구 할인율을 계산해 주는 기능 구현 * feat: +, - 버튼으로 총원을 조절하는 기능 구현 * fix: 할인율과 엔빵가격 계산 시 0으로 나눠지는 상황을 제거 * fix: 맞춤법 수정 할인률 -> 할인율 * fix: 총원 버튼 크기가 너무 작아서 확대 * fix: 항목간 간격이 좁아서 확대 * refactor: Offering Write의 API service, DataSource, Repository를 Offerings와 합침 * refactor: 디버깅용 코드 삭제 * refactor: 버튼 활성화/비활성화를 selector와 삼항연산자로 구현 * refactor: 바인딩어댑터 대신 뷰모델이 visibility 상태를 갖고 있는 방식으로 변경 * refactor: 바인딩어댑터 대신 xml에서 처리하는 방식으로 변경 * refactor: 총원 디폴트 라이브데이터값 상수화 * refactor: +, - 텍스트뷰 버튼으로 수정 * refactor: textStyle bold대신 fontFamily suit_bold를 쓰는 것으로 수정 * refactor: 변수명 뒤에 Int를 붙이는 것 대신 Value를 붙이는 것으로 수정 * refactor: 글작성 제출 버튼의 아이디를 추가 * refactor: ktFormat * refactor: 토스트를 띄우는 함수 분리 * refactor: 도메인 객체 분리 * refactor: UI모델 적용 * refactor: ktFormat 적용 * feat: 댓글방 디테일 Room을 사용하여 data 저장 (#166) * feat: local database 구현 * feat: entity 구현 * feat: dao 구현 * feat: LocalDataSourceImpl 구현 * feat: entity mapper 구현 * refactor: CommentResponse 에 id 값 추가 * refactor: datasource 이름 변경 및 패키지 변경 * refactor: article -> offering으로 네이밍 변경 * refactor: repository 패키지 변경에 따른 수정 * refactor: datasource 패키지 변경 및 local 과 remote 분리 * refactor: repository Application 클래스를 통한 주입으로 변경 * style: ktlint 적용 * refactor: api service 리네이밍 * refactor: git conflict 해결 * refactor: 함수 이름 컨벤션에 맞도록 변경 (getMeetings -> fetchMeetings) * chore: CI 스크립트 추가 (#173) * chore: ci 스크립트 추가 * chore: ci 스크립트 수정 * fix: og 태그 추출 시 크롤링 이슈 해결 (#174) * feat: 날짜, 시간 선택 기능 구현, 주소검색 기능 연결 (#171) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 모집마감 시간 클릭 시 date time picker를 띄우는 기능 구현 * feat: 날짜, 시간 선택 기능 구현 * feat: 주소 검색 기능 연결 * refactor: 함수명 수정, 함수분리 * refactor: ktFormat 적용 * refactor: string으로 분리, 상수화 * fix: string 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정 * chore: CI workflow 파일 수정3 * chore: CI workflow 파일 수정4 * feat: 공모가 정상적으로 게시되었을 시 "공모가 게시되었어요!" 라는 토스트를 띄우고 공모글 작성 프래그먼트를 종료하는 기능 구현 * feat: 토스트가 화면 중앙에 뜨는 문제 수정 * refactor: 사용되지 않는 파일 삭제 * refactor: xml 뷰 id 수정 * refactor: 버튼이 TextView인 문제 수정 * refactor: 사용되지 않는 data binding variable 제거 * refactor: 함수명 수정 * refactor: 다이얼로그, dateTimePickerBinding 전역으로 선언 * refactor: dateTimePicker 클릭 이벤트를 추상화 해 xml에서 처리하도록 변경 * refactor: ktFormat * feat: 상품 URL 이미지 추출 API 연결 (#180) * refactor: 사용하지 않는 파일 제거 * refactor: 가시성 변경 * feat: api service 구현 * feat: datasource 구현 * refactor: repository 네이밍 수정 (offeringsRepository -> offeringRepository) * feat: 사진 업로드 관련 리소스 파일 추가 * feat: repository 및 model 구현 * feat: 이미지 링크를 통한 크롤링 이미지 불러오는 api 연결 및 이미지 삭제 로직 구현 * style: ktlint 적용 * refactor: 이미지 prefix 추가 및 에러 메시지 수정 * refactor: build 오류 수정 * fix: git conflict 해결 * feat: 공모 목록 조회 API에 필터링과 검색 기능 추가 (#169) * feat: 공모 필터 목록 조회 API 구현 * test: 공모 필터 목록 조회 API 테스트 * style: 개행 형식 통일 * feat: 공모 필터 목록 조회 API Specification 도입 준비 * fix: url에 큰따움표 제거 * feat: Specification 도입 * refactor: queryString 구체화 * refactor: 함수명 변경 * feat: 최신순 필터링 적용 * feat: 마감임박순 필터링 적용 * feat: 높은할인률순 필터링 적용 * refactor: 전략 패턴 적용해 여러 갈래의 분기문과 중복되는 코드 처리 * test: 변경된 API 스펙에 맞게 문서화 작업 * refactor: 관련있는 메서드들끼리 모이게 순서 재배치 * refactor: 맞춤법 수정 * style: 개행 제거 --------- Co-authored-by: masonkimseoul * feat: 상태 변경 API 구현 (#175) * feat: 댓글방 상태 변경 및 조회 API 구현 Co-authored-by: masonkimseoul * feat: 공모글 상태 조회 API 구현 * feat: 댓글방 상태 변경 중 수동 확정 기능 구현 * refactor: 상태 변경 관련 메서드명 수정 * refactor: 추상 클래스 메서드 컨벤션 통일 * refactor: errorCode 사용 시 클래스 명시 * refactor: 댓글방 상태 관련 API 엔드포인트 수정 및 패키지 변경 * refactor: 댓글방 상태 변경 API HTTP 메서드 수정 * feat: 공모 모집 자동 확정 시 댓글방 상태 변경 --------- Co-authored-by: masonkimseoul Co-authored-by: Choo * feat: 로그인 기능 구현 (#177) * feat: password 일방향 암호화 기능 구현 * feat: cookie 생산-소비 기능 구현 * chore: jwt 관련 의존성 추가 * feat: 토큰 생성 기능 구현 * feat: 로그인 API 구현 * test: 로그인 API 테스트 * feat: 회원가입 API 구현 * test: 회원가입 API 테스트 * feat: 닉네임 생성 기능 구현 * test: 닉네임 생성 기능 테스트 * fix: postconstruct 여러 개라 발생한 에러 해결 * feat: 회원가입 응답값에 랜덤생성한 닉네임 추가 * feat: MemberArgumentResolver 구현 * feat: MemberArgumentResolver 일부 적용 * test: 바뀐 스펙에 맞게 변경 * test: TestConfig 설정해 빈충돌 오류 해결 * test: 공모 작성 API로 MemberArgumentResolver 사용 * feat: 토큰 재발급 API 구현 * test: 토큰 재발급 API 테스트 * test: 토큰 재발급 API 에러 테스트 * feat: MemberArgumentResolver commant에 적용 * feat: MemberArgumentResolver offering에 적용 * feat: MemberArgumentResolver participant에 적용 * refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경 * refactor: 클래스명 일관적으로 변경 * refactor: 직관적인 명명으로 enum 네이밍 변경 * refactor: Custom Exception 적용 * refactor: 컨트롤러 메서드에 접근제어자 명시 * fix: 중복된 enum 값 제거 * test: 바뀐 API 스펙에 맞게 변경 --------- Co-authored-by: fromitive * fix: nicknameWordInitializer 설정 오류 해결 (#182) * fix: keyword null일 때 처리 및 docs에서 required 제거 (#184) * fix: keyword null일 때 처리 * test: optional() 붙여서 required 제거 * chore: 브랜치에 상관없이 pr 머지 시 자동으로 관련 이슈 닫는 스크립트 구현 (#187) * fix: og 이미지 태그 크롤링 문제 해결 (#190) * refactor: 댓글방 상태 도메인 설계 변경 (#189) * feat: 공모 목록 API 응답값에 낱개 가격 추가 (#193) * chore: readtimeout 5초로 수정 (#195) * feat: 댓글방 상태 조회 시 상태별 이미지 함께 반환 (#196) * feat: 공모 목록 조회 API연결 (#201) * refactor: Condition 수정에 따른 변경 * refactor: api변경에 따른 리팩토링 * refactor: api변경에 따른 목록 무한 스크롤 기능 리팩토링 * feat: 검색 기능 구현 * feat: 필터링 기능 구현 - 참여 가능은 서버 에러로 추후 추가 예정 * feat: 아이템을 불러온 후 recyclerview의 최상단으로 이동하는 기능 구현 - 검색, 필터링 수행 후 최상단으로 이동 * feat: 필터링 목록 불러오는 api연결 * feat: 마감임박 상태 추가 * refactor: default parameter제거 * style: lint적용 * feat: 토큰 반환 시 cookie가 아닌 body 사용하도록 변경 (#206) * feat: 발급한 토큰을 header가 아닌 body로 반환하도록 수정 * refactor: 사용안하는 클래스와 메서드 제거 * test: 바뀐 API 스펙에 맞게 명세 수정 * feat: 이미지 더미 데이터 수정 및 부정확한 가격 데이터 수정 (#207) * refactor: 공모 글 작성 시 총대 참여자 추가 (#208) * feat: 바텀 네비게이션 고정 기능 구현 (#211) * feat: 데이터에서 5자 이상 제거 (#212) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 (#202) * feat: n빵 가격이 낱개가격보다 큰경우 예외가 발생하도록 변경 * refactor: 도메인 명칭 변경 (낱개가격 -> 원가격) * refactor: 도메인 명칭 변경 (공모 -> 댓글방) * refactor: originPrice로 http client 변경 * feat: 키보드 이외 영역 터치 시 키보드 내려가도록 구현 (#214) * feat: 키보드외 화면 클릭 시 키보드 내려가도록 구현 * refactor: api변경에 다른 dto수정 * feat: 이미지 업로드 및 권한 설정 (#216) * chore: 이미지 권한 추가 * feat: permission manager을 생성하여 권한 체크 및 request * feat: 이미지 추가 버튼을 클릭할 시 권한 설정 연결 * feat: 이미지 피커를 사용하여 uri 전달 구현 * feat: 이미지 파일 업로드 api service 구현 * feat: 이미지 파일 업로드 data source 구현 * feat: 이미지 파일 업로드 repository 구현 * feat: 이미지 파일 martipart로 변환해주는 기능 구현 * feat: 이미지 업로드 관련 뷰 수정 * feat: 이미지 파일 업로드 및 api 연결 구현 * style: ktlint format * fix: git conflict 해결 * refactor: 이미지 scaleType 변경 * refactor: string value 컨벤션 적용 * feat: 토큰 반환 시 body가 아닌 cookie로 반환하도록 원상복구 (#223) * feat: 토큰 재발급 API에서 requestHeader로 refreshToken 받도록 수정 (#227) * feat: 토큰 재발급 API에서 body가 아닌 cookie로 토큰 반환 * feat: 회원가입 API도 body가 아닌 cookie로 토큰 반환 * refactor: service 용 dto 명 컨벤션에 맞춰 수정 * feat: 댓글방 일정 수정 API 구현 (#226) * feat: 댓글방 일정 수정 API 구현 * test: 총대가 아닌 참여자가 공모 일정 정보를 수정할 경우 예외 발생 * feat: 댓글방 상태 조회 시 버튼 텍스트 추가 (#229) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 (#222) * feat: 검색 시 해당 키워드의 색상을 변경하는 기능 구현 * refactor: 구현 방식 변경 * style: lint적용 * Feature/217 offering status (#230) * feat: 댓글방 상태 조회 api service 구현 * feat: 댓글방 상태 조회 model 및 dto 구현 * feat: 댓글방 상태 조회 datasource 구현 * feat: 댓글방 상태 조회 repository 구현 * feat: 댓글방 상태 조회 api 연결 구현 * style: ktlint 적용 * feat: 댓글방 상태 변경 (#231) * feat: 댓글방 상태 변경 api service 구현 * feat: 댓글방 상태 변경 data source 구현 * Revert "feat: 댓글방 상태 변경 data source 구현" This reverts commit 052691a8de945c60a60586ee66a05a6a3b264217. * feat: 댓글방 상태 변경 data source 구현 * feat: 댓글방 상태 변경 repository 구현 * feat: 댓글방 상태 변경 api 연결 구현 * style: ktlint 적용 * feature: 카카오 로그인 구현 (#235) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * feat: 공모 참여자 목록 조회 API 구현 (#225) * feat: 공모 참여자 목록 조회 API 구현 * test: 실패 테스트 오류 수정 * style: 띄어쓰기 적용 * refactor: MemberEntity를 받도록 변경 * refactor: isParticipant를 구현하여 가독성 개선 * refactor: 총대를 찾을 수 없는 상황의 예외 추가 * refactor: 참여 검증로직을 서비스로 이동 * refactor: 사용하지 않는 메서드 제거 * refactor: 검증 로직 가장 상단에 위치 * refactor: 총대 추출 로직 수정 --------- Co-authored-by: masonkimseoul Co-authored-by: SCY * refactor: 마감임박순 필터링 쿼리 조건 수정 (#239) * refactor: 마감임박순 필터링 조건 수정 * refactor: 더미 데이터 시간 수정 * fix: 필터링 오류 수정 (#243) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 (#247) * feat: 공동구매 상태 변경 다이얼로그 구현 (#245) * feat: 공동구매 상태 변경 다이얼로그 view 구현 * feat: 공동구매 상태 변경 다이얼로그 Listener 구현 * feat: 공동구매 상태 변경 다이얼로그 연결 및 상태 변경 로직 수정 * test: 테스트 코드 작성을 위한 기본 세팅 (#255) * feat: CoroutinesTestExtension 구현 * feat: Livedata getOrAwaitValue 구현 * feat: InstantTaskExecutorExtension 구현 * feat: TestFixture 생성 * style: ktlint 적용 * feat: 공모글 목록 화면 UI 개선, 공모글 작성에서 낱개 금액이 엔빵 가격보다 저렴할 시 글 작성 막는 기능 구현 (#246) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * feat: 카카오 로그인 기능 초기 설정 * feat: 카카오 로그인 기능 구현 * feat: 카카오 로그인 UI 구현 * feat: 카카오 로그인 구현 * feat: 카카오 로그인 - 회원가입 기능 구현 * feat: 카카오 로그인 버튼 이미지 다운로드 * refactor: 함수명 수정 * refactor: 필요 없는 파일 제거 * refactor: 패키지 이동 * feat: 데이터 스토어에 memberId, nickName 저장하는 기능 구현 * feat: 로그인 post 기능 구현 * feat: 로그인 시도 후 실패할 경우 회원가입 하는 기능 구현 * fix: 바뀐 auth api 적용 * feat: 서기 pr 충돌 해결 * fix: api 필드명 수정 * refactor: ktFormat 적용 * fix: 테스트용 임의 문자열 제거 * feat: CookieJar 구현 * feat: API 수정에 맞춰 서비스 함수 수정 * refactor: 사용되지 않는 코드 제거 * refactor: http 상태 코드 enum 클래스로 묶음 * fix: 구분선을 각각의 아이템의 하단에 넣고 프래그먼트 뷰의 "채팅" 텍스트 밑에 하나 추가 * fix: 텍스트뷰에 font 적용, 마지막 댓글 시간 텍스트를 조금 왼쪽으로 이동 * fix: 낱개 가격 이름을 eachPrice -> originPrice 수정 * fix: 낱개 가격이 엔빵 가격보다 싸면 토스트를 띄우고 글작성을 막는 기능 구현 * fix: 네이티브앱키 로컬프로퍼티로 이동 * refactor: 함수명 변경 * fix: 카카오 계정으로 로그인 후 액티비티 전환하지 않는 문제 수정 * refactor: 사용되지 않는 클래스 삭제 * refactor: 패키지 수정 * refactor: alsong 로그 수정 * refactor: 변수명 수정 * refactor: Manifest의 네이티브앱 키 숨김 * refactor: 로컬프로퍼티의 데이터 형식 수정 * Update android.yml * refactor: alsong 로그 삭제 * ci 빌드 실패가 manifest때문인지 테스트 * refactor: 매니페스트에 앱 키 넣을 수 있게 하는 gradle 설정 수정 * 매니페스트 수정하고 재테스트 * 매니페스트 수정하고 재테스트 * chore: 그래들 수정 * chore: 그래들 수정2 * chore: 그래들 수정3 * chore: 그래들 수정4 * chore: 카카오 계정으로 로그인하는 기능 제외 * feat: 홈화면 테스트 작성 (#257) * chore: mockk의존성 추가 * test: OfferingViewModel 테스트 작성 * style: lint적용 * refactor: stub를 TestFixture로 이동 * test: 댓글방 테스트 코드 작성 (#258) * refactor: 댓글 보내는 함수명 변경 * refactor: 공구 약속 장소 및 시간 캐시 기능 * test: 테스트를 위한 fake repository 구현 * test: 댓글방 viewmodel test 작성 * feat: 댓글방 ActivityTest 작성 * feat: 댓글방 ActivityTest 작성 * style: ktlint 적용 * refactor: test fixture에서 사용하지 않는 것 삭제 * style: ktlint 적용 * feat: GA 모니터링 환경 구축 및 로깅 전략 적용 (#242) * chore: Firebase Crashlytics 의존성 추가 * feat: Firebase 초기화 * feat: FirebaseManager 구현 * feat: 총대가 공구 진행 상황을 다음 단계로 변경했을 때 event 추가 * feat: 로깅 기능 구현 - 검색 - 필터링 - 공모글 클릭 - 공모 참여 * style: lint적용 * feat: 글 작성 완료 시 event 추가 * feat: 로그인 시 event 추가 --------- Co-authored-by: Namyunsuk Co-authored-by: songpink * test: 공모글 작성 이미지 테스트 코드 작성 (#260) * refactor: 상수 가시성 변경 * feat: test fixture 구현 * feat: fake repository 이미지 업로드 기능 추가 * test: OfferingWriteViewModelTest 이미지 업로드 test 코드 작성 * feat: 로그인 후 홈화면으로 이동해도 로그인 화면이 종료되지 않는 문제 수정 (#261) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 로그인 후 LoginActivity가 종료되도록 수정 * feat: 공모 상세 화면 테스트 작성 (#264) * feat: OfferingDetailViewModel 테스트 작성 * refactor: 테스트 수정 * style: lint적용 * style: lint적용 * feat: 로깅 코드 삽입 (#266) * fix: 원 가격이 없는 경우 n빵 가격을 비교하지 않도록 변경 * feature: 로깅 샘플 구현 * refactor: 불필요한 코드 제거 * feat: logging 적용 --------- Co-authored-by: fromitive * fix: 마감 임박 필터링 쿼리 수정 (#267) * chore: logback 설정 진행 (#270) * chore: logback 설정 * fix: multipart 요청 필터링 * chore: logback 설정 변경 * chore: pull request ci/cd 닫기 * fix: 이미지 업로드 API의 responseBody가 두 번 뜨는 오류 해결 (#273) * fix: 이미지 업로드 API 두 번 도는 문제 해결 * test: 이미지 업로드 API의 누락된 response field 추가 * refactor: 홈화면 수정 (#271) * refactor: 할인율 마진 추가 * refactor: 공구상태에 대한 문구 수정 * refactor: 클릭 시 최상단으로 이동하는 버튼 구현 * feat: 공모글 작성 화면 테스트코드 작성 (#274) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: 공모글 작성 테스트 구현 * feat: 댓글방 목록 화면 테스트코드 작성 (#276) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * test: "댓글방 목록을 확인할 수 있어야 한다" 테스트 작성 * feat: pageSize validation 추가 (#279) * feat: pageSize validation 추가 * feat: magic number 추출 * fix: 공모 상세 화면 오류 수정 (#280) * fix: 총대 여부 확인 로직 수정 * fix: 마감 임박 시 보여주는 버튼 수정 * fix: 공모 작성 후 홈화면으로 돌아왔을 떄 목록이 새로고침 되지 않는 오류 수정 * test: 테스트 코드 수정 * style: lint적용 * feat: 댓글방 목록 화면 자동 업데이트 되지 않는 문제 수정, 회원가입 이후 자동으로 로그인되지 않는 문제 수정 (#282) * refactor: 뷰모델 팩토리를 뷰모델의 companion object에서 구현하는 방식으로 변경 * fix: 라이플사이클 오너 설정 * fix: 회원가입 후 자동으로 로그인 되도록 수정 * chore: change version name (#291) * feat: 카카오 계정 로그인 기능 구현 시 CI가 실패하는 문제 해결 (#296) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * feat: 로그인 화면 리팩토링 (#298) * fix: ci가 실패하는 문제 수정(오타수정..) * fix: 카카오 계정 로그인 기능 추가 * refactor: SimpleCookieJar의 패키지 변경(presentation 레이어에서 data레이어의 source 패키지로 이동) * refactor: data store를 관리하는 클래스를 생성하고 이 클래스를 사용하도록 변경 * refactor: 사용하지 않는 의존성과 주석 제거 * refactor: http status code 추가 * refactor: 함수분리 * refactor: ktFormat 적용 * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentRooms) * feat: 액세스 토큰 만료 시 토큰 재발급 기능 구현(CommentDetail), 사용되지 않게 된 memberId 제거 * refactor: ktFormat 적용 * test: 테스트코드 수정 * refactor: Preferences -> DataStore 이름 변경 * refactor: 채팅방 UI UX 개선 (#303) * feat: 키보드가 아닌 다른 영역을 클릭하면 키보드 내리는 기능 구현 * feat: 뒤로가는 버튼 기능 추가 * feat: 댓글 입력 maxLines 설정 및 maxLength 설정 * style: ktlint 적용 * 필요 없는 코드 제거 * feat: 댓글방 목록에서 자신이 총대인 댓글방의 UI 개선 (#304) * refactor: 댓글방의 자신이 총대인 댓글방 ui 개선 * fix: Binding 클래스 네이밍 수정 * feat: 가로모드, 다크모드 설정 (#305) * refactor: api변경에 따른 리팩토링 (#310) * feat: 로그인 화면 해상도 대응 (#313) * feat: 이미지 업로드 중일 때 로딩 상태 설정 (#317) * feat: 공모 글 작성 ui state 구현 * feat: 로딩 progressbar 생성 * feat: UI 상태에 따른 토스트 메시지 처리 * refactor: 잘못된 입력에 대한 에러 처리 변경 * refactor: 홈화면 리팩토링 (#324) * refactor: textSize dp로 변경 * refactor: 검색 버튼 크기 변경 - 검색 버튼 패딩 추가 - 검색창 끝에 패딩 추가 * refactor: 엔터키를 통해 검색하도록 수정 * refactor: 필터 단일 선택되도록 수정 * style: lint적용 * feat: 댓글방 새로운 기능 GA 연결 (#328) * feat: 댓글방 참여자 확인 Event 구현 * feat: 댓글방 상태 변경 다이얼로그 취소 Event * feat: 참여자가 공구에서 참여 포기 Event 구현 * style: ktlint 적용 * test: 테스트 데이터 수정 (#330) * feat: Fragment GA 모니터링 수집 (#332) * feat: fragment logScreenView 추적 함수 구현 * feat: 각 fragment에서 화면 감지 GA 설정 * feat: 마이페이지 기본 세팅 및 뷰 변경 (#335) * feat: 공모 참여 취소 기능 구현 (#318) * test: 공모 참여 취소 테스트코드 작성 * feat: 공모 참여 취소 기능 구현 * refactor: 불필요한 쿼리 메서드 제거 * style: 불필요한 개행 제거 * refactor: 모집중인 상태가 아닌 경우 공모 참여를 취소할 수 없도록 변경 * refactor: 공모 참여 취소 응답 상태 코드 변경 * refactor: 에러 메시지 명확한 문구로 변경 * refactor: query parameter를 적용해 어떤 공모의 참여를 취소할 것인지 의도를 명확하게 전달하도록 변경 * refactor: 총대 검증 메서드 네이밍 명확하게 변경 * feat: 댓글방 생성 시점 변경 (#319) * feat: 댓글방 생성 시점 변경 * refactor: 불필요한 도메인 OfferingWithRole 제거 * refactor: 불필요한 도메인 CommentWithRole 제거 * refactor: 댓글의 작성자 확인 메서드 추가 * refactor: 댓글방 목록 조회 dto 생성자 추가 * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 (#322) * feat: 로그인 API 응답에 memberId와 nickname 필드 추가 * refactor: 로그인용 dto 분리 및 공통 dto에 prefix로 auth 추가 * feat: valid 어노테이션 추가 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 (#323) * refactor: 메서드명 구체적으로 변경 * refactor: 변수명 구체적으로 변경 * feat: 공모 상세 조회 API 응답에 총대여부 알려주는 boolean 필드 추가 * docs: todo 추가 * refactor: 함수명 통일 * feat: 공모자 여부 필드명 변경 * feat: 댓글방 상태 조회 API 확장 (#325) * feat: 댓글방 상태 조회 API 확장 * refactor: 댓글방 관련 로직 댓글 도메인으로 이동 * feat: LoggingFilter에서 던지는 유효하지 않은 요청에 대한 예외 처리 * refactor: 댓글 관련 엔드포인트 수정 * feat: 댓글방 정보 조회 시 조회 권한을 가진 사용자인지 검증 * refactor: 댓글방 상태 확인 로직 도메인으로 이동 * feat: 상태 변경을 시도하는 사용자가 총대인지 검증 * refactor: 댓글 목록 조회 엔드포인트 수정 * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 (#327) * feat: ParticipantResponse에 참여 인원 현황, 예상 정산 가격 추가 * refactor: Response depth 줄이기 및 DTO 생성자 작성 * fix: imminent 필터 버그 해결 (#337) * fix: 커스텀 필터로 인해 h2-console 접속 깨지는 이슈 해결 (#339) * feat: 마이페이지 기능 구현 (#341) * feat: 마이페이지 닉네임 기능 구현 * feat: 로그아웃 로직 구현 * feat: url 연결 로직 구현 * feat: 필요없는 기능 삭제 * style: ktlint 적용 * feat: 공모 테이블에 할인율과 상태 필드 추가 (#342) * refactor: Condition과 Status 이름 변경 * refactor: 사용하지 않는 DTO 제거 * feat: OfferingEntity에 칼럼 추가 * feat: 공모 거래 날짜 필드 이름 변경 (#348) * fix: 상세화면에서 홈화면으로 갔을 때 상태 변경 안되는 오류 수정 (#343) * refactor: 공모상세페이지 Activity -> Fragment로 리팩토링 * fix: 페이지네이션 및 상태변경 미적용 오류 해결 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 주석 제거 및 상수화 * refactor: livedata 자료형 변경 * refactor: progressbar위치 수정 * refactor: lifecycleScope사용 리팩토링 * refactor: adapter에서 전체 아이템이 아닌 특정 아이템만 notify하도록 리팩토링 * refactor: API변경에 따른 대응 (#352) * refactor: api대응 * refactor: api변경에 따른 테스트 수정 * feat: 공모글 작성 화면 ux 개선 (#344) * fix: 각 항목의 설명을 place holder로 이동 * fix: 필수와 선택 항목의 프래그먼트 분리 * feat: 버튼이 항상 보이도록 수정 * fix: 가격과 총원은 숫자만 입력받도록 변경 * fix: 패딩 수정 * fix: ui 수정 * fix: 도메인 변경에 따른 deadline -> tradeDate 수정 * feat: 필수 항목을 모두 입력하면 선택 항목 화면으로 이동하는 기능 구현 * refactor: ktFormat 적용 * refactor: shared viewModel 사용, 미필수 항목을 미필수 입력 화면으로 이동 * refactor: 프래그먼트 이름 변경 * feat: 입력 숫자의 글자수와 라인수 제한 기능 구현 * fix: 총원이 -1이하로 떨어지는 버그 수정, 공동구매 텍스트 띄어쓰기 제거 * fix: 할인율, 엔빵 금액이 유효하지 않을 때는 "-"로 뜨도록 변경 * fix: 공모를 게시하면 필수, 선택 화면 모두 종료되도록 수정 * fix: 날짜 시간 픽커를 날짜만 선택하는 픽커로 변경 * refactor: ktFormat 적용 * refactor: 바인딩어댑터의 파라미터를 nullable하게 수정 * test: 테스트코드 수정 * feat: 낱개 가격의 place holder로 현재 엔빵 금액을 보여주는 기능 구현 * feat: 내용의 최대 글자수와 현재 글자수를 보여주는 기능 구현 * refactor: ktFormat 적용 * refactor: 공모글 작성시 memberId를 보내지 않도록 변경 * fix: 총원 최대 4자리에서 3자리까지만 입력받을 수 있도록 변경 * fix: deadline -> meetingDate 네이밍 수정 * fix: 공모글 작성 후 작성 화면의 입력값이 초기화되지 않는 버그 수정 * refactor: 네이밍 수정(eachPrice -> originPrice) * refactor: 네이밍 수정(individualPrice -> originPrice) * fix: 내용의 현재 글자수 색이 메인컬러가 되지 않는 문제 수정 * refactor: 프래그먼트 종료될 때 바인딩 해제하도록 수정 * refactor: id가 없는 뷰의 id 추가 * refactor: 함수 분리 * fix: 내용 옆의 * 제거 * fix: GA 이벤트 이름 변경(공모글 작성 - 필수 화면에서의 이벤트임을 명시함) * refactor: og 태그 추출 기능 수정 (#349) * refactor: crawler 패키지 이동 * feat: naver api 클라이언트 추가 refactor: 사용하지 않은 기존 og image 크롤러 명칭 변경 * feat: html 크롤링 방식과 naver api 방식을 조합하는 Extractor 구현 * fix: OfferingService ProductImageExtractor 추상화 * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 (#358) * feat: 로그인 시에도 memberId와 nickName을 받아서 data store에 저장하는 기능 구현 * test: 테스트코드 수정 * refactor: 공모글 목록 조회 필터링 수정 및 추가 (#356) * refactor: 마감임박순 필터링 이름 마감임박만으로 변경 Co-authored-by: fromitive * refactor: 필터링 쿼리 수정 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 구현 Co-authored-by: fromitive * feat: "참여가능만" 필터링 기능 연결 Co-authored-by: fromitive * fix: 쿼리 내 불필요한 파라미터 제거 Co-authored-by: fromitive * refactor: 할인율이 null일 경우 높은할인율 필터링 대상에서 제외 Co-authored-by: fromitive * feat: 참여가능만 필터링 전략 클래스 추가 * feat: 공모 목록 조회 API 응답값 변경 * fix: 높은 할인율 단위 변경 및 last-id 필터링 로직 수정 * style: 주석 제거 --------- Co-authored-by: fromitive * refactor: 할인율 계산 로직 수정 (#359) * refactor: 할인율 계산 로직 수정 Co-authored-by: fromitive * refactor: 소수점 둘째 자리에서 반올림하도록 변경 Co-authored-by: fromitive * test: 할인율 계산 로직 * fix: 할인율 단위 백분율로 수정 --------- Co-authored-by: fromitive * feat: 총 모집 인원 수 최댓값 설정 (#361) Co-authored-by: fromitive * fix: 필터 오류 수정 (#362) * fix: 필터 오류 수정 - '참여가능만'필터 분기처리 제거 * chore: 주석 제거 * feat: API 스펙 변경에 따른 대응 (#364) * feat: 댓글 목록 조회 api 스펙 변경에 따른 대응 * feat: 댓글방 정보 조회 api 스펙 변경에 따른 대응 * feat: 공모 일정 조회 api 스펙 변경에 따른 대응 * feat: 댓글 상태 변경 api 스펙 변경에 따른 대응 * test: api 스펙 변경에 따른 test 코드 변경 * style: ktlint 적용 * feat: remote dto package 분리 * feat: 자동 확정 기능을 위해 스케줄러 적용 (#363) * chore: todo 추가 및 메서드명 변경 * feat: Scheduled 어노테이션 추가 및 Scheduler 분리 * test: ServiceTest 환경 구축 * feat: offeringStatus 변경 로직 추가 * refactor: 수동 확정 로직 추가 및 코드 스타일 수정 * refactor: 자동 확정 로직을 조회에서 Scheduled로 이동 * fix: 마감임박 설정 기준 내일로 변경 --------- Co-authored-by: Choo Co-authored-by: SCY * fix: 공모 작성 후 홈화면 돌아올 때 새로 작성한 글이 보이지 않는 오류 수정 (#369) * feat: Access Token, Refresh Token을 data store에 저장하는 기능 구현 (#372) * feat: 앱 재시작 시 토큰을 데이터스토어에서 꺼내 사용하는 기능 구현 * feat: 로그인이 이미 되어있다면 로그인 화면을 건너뛰는 기능 구현 * feat: 로그아웃 기능 구현 * fix: 마이페이지 화면으로 넘어가면 바텀네비게이션이 사라지는 버그 수정 * fix: 데이터스토어에서 토큰이 꺼내지지 않는 버그 수정 data store에서 토큰을 꺼내는 코루틴 비동기 작업이 끝나기 전에 함수를 종료해 버려서 생기는 버그였습니다. * refactor: ktFormat 적용 * refactor: startActivity 함수를 LoginActivity가 동반객체로 갖고 있도록 변경 * refactor: 함수명과 event명 변경 추가로 GA위치가 조금 잘못된 점이 있어서 수정했습니다. * feat: 공모 상세 화면 추가 기능 반영 (#375) * feat: 신고하기 기능 구현 * feat: 물품 링크가 없으면 보여지지 않도록 구현 * refactor: 마감 시간에서 거래 날짜로 리팩토링 * feat: 이미 참여한 공모게시글에서 채팅방으로 이동하는 기능 구현 * fix: 댓글방 목록의 마지막 댓글방이 보이지 않는 문제 수정 (#376) * fix: 리사이클러뷰 레이아웃의 크기가 화면 밖에 벗어나지 않도록 수정 * fix: 리사이클러뷰 레이아웃의 맨 밑에 구분선 하나 추가 아래로 땡겼을 때 구분선이 사라져버리는게 보기 안좋아서 추가했습니다 * refactor: 코트 포맷 적용 (컨트롤 알트 L) * feat: isManualConfirmed 제거 및 도메인 로직 확인 (#377) * refactor: isManualConfirmed 칼럼 삭제 및 관련 로직 분리 * refactor: 더미 데이터 수정 --------- Co-authored-by: fromitive * feat: API 별 권한 확인 로직 추가 (#371) * feat: 권한 확인 로직 추가 * feat: 인증 필터 적용 * refactor: 더미 데이터 칼럼 위치 변경 (#382) * refactor: 홈화면 api필드 추가에 따른 대응 (#381) * refactor: dto필드 추가 * fix: 상태 변경 오류 해결 * fix: 필터 선택 또는 검색상태일 때 공모 작성 후 나오면 목록 안보이는 오류 수정 * refactor: 세부 주소 api에서 받아오도록 변경 * style: lint적용 * fix: API 문서에 접근할 수 없는 현상 해결 (#384) * fix: API 문서에 접근할 수 없는 현상 해결 * style: 신뢰할 수 있는 URL 개행 수정 * feat: 공모 목록에서 동을 보여주는 기능 구현 (#386) * feat: 공모 단건 조회 API 구현 (#388) * feat: 공모 상세 조회 API 엔드포인트 변경 * feat: 공모 단건 조회 API * style: 공모 관련 API 순서 변경 * test: 불필요한 공모글 생성 코드 제거 * test: 공모 단건 조회 서비스 테스트 * refactor: 상태변경 리팩토링 (#389) * refactor: 공모 상세 조회 api변경 대응 * refactor: 공모 상태 변경 리팩토링 * refactor: 리팩토링에 따른 테스트 수정 * chore: 불필요한 로그 제거 * fix: 댓글 입력 후 뒤로가기 시 최근 댓글이 반영되도록 수정 (#397) * chore: JAR 파일에 OAS 파일 누락되는 이슈 해결 및 중복 task 제거 (#391) * chore: 중복되는 task 제거 * chore: cicd 범위 조정 * fix: 참여자 목록 조회 API에서 totalCount 반환하지 않는 이슈 해결 (#400) * feat: 댓글방 참여자 확인 API 연결 (#401) * feat: 참가자 정보를 가져오는 api service 구현 * refactor: 필요없는 코드 삭제 * feat: 참여 관리 datasource 구현 * feat: 참여자 domain 모델 구현 * feat: 참여를 관리하는 repository 구현 * feat: 참여자 목록을 보여주는 recycler view 연결 및 구현 * refactor: 더보기 버튼 수정 * feat: 필요없는 리소스 파일 삭제 및 상태 기본 이미지 변경 * refactor: 약속 장소 및 시간 ui model 을 사용하여 관리 * refactor: 댓글방의 정보를 불러오는 로직 ui model을 사용하여 관리 * refactor: ui model 변환 로직 변경 * feat: 공동구매 참여 인원 확인 기능 구현 * feat: 신고하기 폼 연결 구현 * test: 코드 변경에 따른 테스트 코드 수정 * style: ktlint 적용 * refactor: xml id 추가 * feat: 댓글방 공동구매 나가기 API 연결 (#402) * feat: 공동구매 나가기 기능 api service 구현 * feat: 공동구매 나가기 기능 data source 구현 * feat: 공동구매 나가기 기능 repository 구현 * feat: 공동구매 나가기 기능 연결 * style:ktlint 적용 * fix: /auth/refresh endpoint accessToken 검증 예외 추가 (#407) * refactor: 더미 데이터 정합성 확보 (#406) * refactor: 더미 데이터 정합성 확보 * refactor: 추가된 칼럼 반영 * feat: CallApiHandler 구현 (#403) * feat: CallApiHandler 구현 * refactor: CommentRoomsDataSource 수정 * feat: CommentRemoteDataSourceImpl 에러핸들링을 통해 수정 * feat: 에러 핸들링에 따른 DataSource 리팩토링 - OfferingDetailDataSource - OfferingRemoteDataSource * feat: ParticipantRemoteDataSourceImpl 에러핸들링을 통해 수정 * style: ktlint 적용 * refactor: AuthRemoteDataSource 수정 * feat: Result의 map 과 getOrThrow 함수 생성 * feat: 에러 핸들링에 따른 Repository 리팩토링 - OfferingDetailRepository - OfferingRepository * refactor: Result 변경에 따른 레포지토리 수정 (AuthRepository, CommentRoomsRepository) * feat: 에러 핸들링에 따른 CommentDetailRepository 리팩토링 * feat: 에러 핸들링에 따른 ParticipantRepository 리팩토링 * feat: 에러 핸들링에 따른 viewmodel 리팩토링 - OfferingViewModel - OfferingDetailViewModel * refactor: 에러 핸들링에 따른 LoginViewModel 리팩토링 * refactor: 에러 핸들링에 따른 CommentRoomsViewModel 리팩토링 * refactor: 토큰 리프레쉬 후 다시 함수 호출하도록 추가 * feat: 에러 핸들링에 따른 CommentDetailViewModel 리팩토링 * refactor: 에러 핸들링에 따른 OfferingWriteViewModel 리팩토링 * refactor: 공모 목록 토큰 리프래시 적용 * fix: 잘못된 코드 수정 * refactor: 필요없는 주석 제거 * refactor: 공모 목록 리팩토링 * fix: 리빌드시 쿠키가 제대로 저장되지 않는 현상 수정 * refactor: 필요없는 코드 삭제 및 상수화 추가 * test: 에러핸들링에 따른 FakeAuthRepository, OfferingWriteViewModelTest 수정 * refactor: ktFormat 적용 * test: 코드 변경에 따른 Fake Repository 변경 * test: CommentDetailViewModelTest 코드 수정 * style: ktlint 적용 * refactor: 가독성 개선(에러 로그 함수명 추가, Success가 Error보다 위에 나오도록 수정) * refactor: 불필요한 로그 제거 * refactor: 리팩토링에 따른 테스트 수정 * refactor: 람다 넘겨주는 방식 수정 * style: lint 적용 * test: 테스트코드 수정 --------- Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: Namyunsuk * feat: proguard를 사용한 난독화 적용 (#413) * chore: 환경에 따른 yml 파일 분리 (#411) * chore: 환경 별로 yml 파일 분리 * chore: 불필요한 yml 설정 제거 * fix: 공구 상세 페이지 오류 해결 (#417) * fix: 바로가기 클릭되지 않는 오류 수정 * refactor: 주소 표시할 때 최대 2줄까지 그리고 넘어갈 시 말줄임 나오도록 수정 * refactor: 공모 목록, 공모 상세 에러 핸들링 (#418) * refactor: 공모 목록에서 401에러를 제외하고는 에러코드 올 시 빈화면 보여주도록 에러핸들링 수정 * refactor: 필터및 업데이트된 공모 목록 가져오는 로직 에러핸들링 수정 - 400: 토스트 메시지 띄어줌 - 401: refresh - 그외에는 로그로 에러 코드를 보여줌 * refactor: strings네이밍 통일 * refactor: 공모 상세 에러 핸들링 수정 * refactor: strings정리 - offering_detail부분 정리 * feat: 카카오 로그인 중 사용자 정보 확인 로직을 안드로이드에서 백엔드로 이관 (#404) * feat: 카카오 로그인 API 구현 * feat: providerId를 loginId로 수정 * feat: 소셜 로그인 시 랜덤 생성된 비밀번호 사용 * refactor: 불필요한 api 제거 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY * test: 로그인 로직 변경 Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY * test: MemberFixture 불필요한 함수 제거 및 통일 Co-authored-by: fromitive Co-authored-by: Dora Choo * refactor: 불필요한 정보 제거 Co-authored-by: fromitive Co-authored-by: Dora Choo * feat: 카카오 로그인 에러 핸들러 추가 Co-authored-by: fromitive Co-authored-by: Dora Choo * feat: 민감 정보 로깅에서 제외 Co-authored-by: fromitive Co-authored-by: Dora Choo --------- Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: SCY Co-authored-by: fromitive * feat: cookie 관련 예외 처리 (#409) * refactor: 더미 데이터 http 추가 (#422) * fix: 더미데이터 정합성 맞추기 (#425) * feat: 로그인 api 변경 반영 (#426) * feat: 카카오 로그인 후 총대마켓 서버로 email을 보내던 방식에서 카카오 access token을 보내는 방식으로 변경 * feat: login과 signup을 하나로 api로 통합된 것 반영 * refactor: ktFormat 적용 * refactor: 테스트코드 수정 * feat: 로깅 시 UUID가 아닌 회원 번호가 기록되도록 변경 (#428) * feat: logging 시 memberId가 나오도록 기능 추가 * feat: logging 시 memberId 및 identifier가 함께 나오도록 변경 * refactor: lombok getter 적용 * feat: Spring Timezone KST로 설정 (#430) * chore: Dockerfile 타임존 변경 (#432) * fix: Offering 목록 조회 시 NPE 해결 (#434) * refactor: 에러… * feat: OfferingEntity Soft Delete 적용 (#483) * feat: 거래 날짜 검증 로직 추가 (#471) * feat: 거래 날짜 검증 로직 추가 * refactor: 검증 로직을 서비스로 이동 * test: 공모 목록 조회 시 soft delete 결과 적용되었는지 확인하는 테스트 추가 (#487) * test: 공모 목록 조회 시 soft delete 결과 적용되었는지 확인하는 테스트 추가 * test: given 주석 누락 수정 * test: 모호한 테스트명 수정 * feat: 공모 수정 API 구현 (#485) * test: 공모 수정 API 문서화 * feat: 총대가 아닌 사용자가 공모를 수정할 때 예외 발생 기능 구현 * feat: 참여 인원수 이하로 총인원을 수정할 수 없는 기능 구현 * feat: 모집날짜가 다음날 이전으로 수정할 수 없는 기능 구현 * feat: 공모 수정 시 n빵 가격보다 작을 경우 수정할 수 없는 기능 구현 * feat: 공모 수정 변경 감지를 위해 트렌젝션 적용 * style: posix 컨벤션 적용 * fix: 테스트 검증 필드 수정 * refactor: 공모 수정 modify -> update로 변경 * style: 불필요한 개행 제거 * refactor: offeringUpdateRequest 의존 제거 todo: Null을 어떤 값으로 채울지 고민하기. null을 허용하는게 옳은가? * fix: 참여 인원이 만석일 때 수정하지 못하는 버그 해결 * refector: 검증 로직 네이밍을 어떤 값을 검증하는지 구체화 하도록 변경 * refector: 검증 로직의 매개변수를 검증하는 대상을 알 수 있도록 구체화 * feat: 수정 성공 시 수정 된 내용을 반환하는 기능 구현 * feat: createMembers 기능 구현 * refector: UpdatedOffering을 추가하여 도메인에서 검증하도록 변경 * feat: 공모 삭제 API 구현 (#489) * feat: 공모 삭제 API 구현 * feat: 거래 진행 중 삭제 시도 시 예외 발생 * test: 거래 완료 시 공모 삭제 가능한 경우 * refactor: 인원 확정 상태 여부 확인 메서드명 변경 * test: 참여자가 삭제를 시도할 경우 실패 * test: 서비스 테스트 Nested 구조 적용 * feat: Spring Actuator로 health check api 생성 (#493) * chore: prod-a, prod-b에 어플리케이션을 배포하도록 변경 (#496) * chore: main branch ci/cd 설정 변경 * chore: 로드 벨런스 대상 포트 변경 * chore: ci/cd deploy 설명 변경 * chore: 포트 번호 변경 * chore: PR 테스트 설정 제거 * chore: 80 포트로 변경 * chore: PR 트리거 제거 * chore: dockerfile 트리거 등록 * feat: 멀티 모듈화를 위한 패키지 분리 (#491) * feat: local 과 remote mapper 분리 * fix: 잘못 import된 코드 수정 * feat: local 및 remote 패키지 분리 * fix: 잘못된 import코드 수정 * feat: common 패키지 생성 및 변경 * feat: auth 패키지 분리 * style: ktlint 적용 * feat: remote 와 local 패키지 data로 이동 * style: ktlint 적용 * feat: error handler 관련 로직 common으로 이동 * refactor: 잘못된 의존성 관계 변경 * feat: DataStore을 common 패키지로 변경 * feat: build.gradle 중복 로직 수정 및 version catalog를 통한 관리 * fix: build 오류 수정 --------- Co-authored-by: Namyunsuk * refactor: 의존성 주입 라이브러리(Hilt) 적용 (#500) * feat: Hilt 의존성 추가 * feat: Auth, CommentRooms Hilt 적용 * feat: Data Store Hilt 적용 * style: ktFormat 적용 * feat: OfferingWrite Hilt 적용 * feat: CommentDetail Hilt 적용 * feat: Participant Hilt 적용 * feat: Assisted Inject 구현 * feat: OfferingDetail Hilt 적용 * feat: ktFormat 적용 * chore: gradle에서 hilt 버전 수정 * refactor: 불필요한 코드 제거 * chore: 중복된 의존성 제거 및 정리 * feat: (빠트린것) MyPageViewModel에 Hilt 적용 * feat: (빠트린것) OfferingDao에도 Hilt 적용 * refactor: 뷰모델 팩토리 제공 함수들 제거 * feat: 채팅방 조회 시 soft delete 된 공모 별도 처리 (#502) * feat: 채팅방 상세 조회 시 isDeleted 필드 추가 * feat: 채팅방 목록 조회 시 삭제된 공모 별도 처리 * feat: Comment-Offering 사이에도 FetchType.LAZY 적용 * refactor: 불필요한 nativeQuery 사용 제거 * refactor: 직접적인 필드 아닌 도메인은 서비스에서 넘겨주도록 수정 * refactor: 불필요한 기존 로직 제거 * docs: httpClient에 추가된 api 반영 * chore: local 환경에서 h2 console 활성화 * feat: isDeleted 필드 제거 * feat: AOP 적용해 읽기/쓰기 DB로 요청 분산 (#503) * chore: 커스텀하게 만든 datasource 사용해 db 연결 * feat: transactional readonly 값에 따라 datasource 변경 * feat: transactional을 writerDatabase 어노테이션으로 대체 * feat: DataSource 관련 동작 prod 프로필로 제한 * feat: WriterDatabase 어노테이션 적용 * feat: writerDatasource 사용 후 readerDatasource로 변경 * feat: writer db와 user db의 권한 분리 * chore: 배포 테스트 * chore: 직렬로 deploy * chore: cicd 범위 원래대로 수정 * chore: osiv 꺼서 인증필터가 사용한 datasource의 영향 제거 * chore: 바뀐 properties 반영 * chore: prod 배포 * chore: cicd 범위 원복 * chore: health-check 요청으로 불필요하게 찍히는 로그 제거 * chore: prod 배포 * chore: cicd 범위 원복 * chore: log 레벨 info에서 debug로 변경 * chore: prod 배포 * chore: cicd 범위 원복 * chore: db 스키마에 대한 validate 검사 * refactor: 불필요한 어노테이션 제거 * refactor: 메서드 순서 사용 순대로 변경 * refactor: OfferingRepository 쿼리 최적화 (#506) * test: osiv 끄면서 깨지는 테스트 수정 (#521) * test: 지연로딩 동작하도록 트랜잭션 어노테이션 추가 * test: 삭제된 필드 restdocs에도 반영 * feat: 읽기 전용 공모 API 구현 (#514) * feat: 공모 상세 정보 조회 시 원가격 정보도 포함 (#517) * feat: 공모 상세 정보 조회 시 원가격 정보도 포함하도록 변경 * style: 개행 제거 * refactor: 필수로 들어있는 필드 설명을 명시 * refactor: 닉네임 생성 명사 파일 변경 (#518) * refactor: 닉네임 생성 명사 파일 변경 * refactor: 닉네임 사용 단어 수정 * test: 닉네임 생성 파일 변경 후 테스트 수정 (#524) * fix: read-only API가 문서화 되지 않은 오류 수정 (#526) * test: 읽기 전용 API 분리 및 문서화 적용 * refactor: test에서 사용하지 않은 필드를 setUp으로 이동 * feat: 총대마켓 읽기 전용 web 페이지 구축 (#528) * test: 읽기 전용 API 분리 및 문서화 적용 * refactor: test에서 사용하지 않은 필드를 setUp으로 이동 * feat: 총대마켓 상세 페이지 기능 구현 * feat: og image url 변경 * refactor: 공'고' -> '모' 로 수정 * feat: 공모 목록 웹페이지 구현 * refactor: 공모 마감 문구 변경 * fix: 필터 변경시 검색어가 초기화 되는 문제 해결 * style: 주석 제거 * feat: 검색 및 필터창 고정하도록 변경 * feat: 타이틀 감성 한 스푼 * feat: 목록 스크롤 시 어색해 보이는 부분 개선 * feat: Offering 쿼리에 Native Query 적용(+ 테스트 코드 수정) (#556) * feat: Soft Delete된 데이터도 검증에 활용할 수 있는 쿼리 구현 * test: 하드 코딩된 날짜로 인해 fail하는 테스트 수정 * refactor: 불필요한 쿼리 삭제 * style: 불필요한 import 삭제 * fix: Native Query 미반영 서비스 코드 수정 (#558) * feat: 딥링크 기능 구현 (#542) * chore: deep-link 테스트 * feat: deeplink redirect 기능 구현 * fix: 페이지가 아닌 URI로 이동되도록 수정 * feat: detail 페이지에 deeplink 적용 * chore: CI/CD 비활성화 * refactor: 딥링크 결정 로직 리팩터링 * style: 개행 컨밴션 맞추기 * refactor: 딥링크 방식 변경 * fix: deeplink 방식 변경 * fix: fallback link 삽입 * refactor: 불필요한 서비스 및 URI 제거 * chore: ci/cd 비활성화 * refactor: else 제거 * fix: 웹 페이지에서 공모 데이터에 선택 필드 값이 존재하지 않을 때 예외 처리 (#546) * fix: 나오지 않은 데이터 예외처리 적용 * feat: 필터 버튼 폰트 색상 변경 * refactor: 머지 해결 * fix: null 처리 해결 (#561) * chore: CI/CD 스크립트 트리거 브랜치 수정 (#567) * chore: 안드로이드 버전 v1.1.2로 변경 (#512) * feat: 공모글 수정 기능 구현 (#522) * feat: 공모글 수정 API Service 함수 정의 * feat: OfferingDetailRepository에 공모글 수정 함수 정의 * feat: 공모 수정 프래그먼트 구현 * refactor: patch 함수 위치 수정 * style: ktFormat 적용 * feat: 수정 기능 구현 * feat: offeringId 주입 구현 * feat: OfferingModifyResponse 구현 * feat: 수정기능 구현(Response 안씀) * feat: 수정 후 OfferingDetail 화면에서 바로 수정된 정보가 보이도록 수정 * feat: 수정 후 OfferingDetail 화면에서 바로 수정된 정보가 보이도록 수정 * style: ktFormat 적용 * feat: 공모글 수정 문구 수정 * feat: 모집 마감 이후에는 공모글 수정이 안되도록 구현 * feat: 수정 버튼 텍스트 변경 * fix: 공모글 작성 중단하고 나왔을 때 작성하던 글이 남아있는 문제 수정 * fix: 상품 url이 빈칸일 경우 서버로 null을 보내도록 변경 * test: patchOffering override * feat: OfferingDetail response에 originPrice 추가 * feat: 공모글 수정 시 원가격이 표시되도록 변경 * fix: originPrice nullable로 수정 * fix: originPrice가 null이면 blank를 edit text에 넣도록 수정 * feat: 공구 삭제 기능 구현 (#532) * feat: 삭제 api추가 * feat: 삭제버튼 클릭 시 나오는 dialog생성 * feat: 삭제 기능 구현 * refactor: 다이얼로그 문자 string로 분리 * chore: 중복 코드 제거 * refactor: Binding Class 사용해 dialogBinding하도록 변경 * refactor: 인터페이스 네이밍 변경 * refactor: 공모 삭제 할 수 없을 대 알림 메세지 수정 * feat: 게시물 삭제 시 toast 띄어주는 기능 추가 * feat: 더블클릭 방지 기능 구현 (#534) * refactor: 더블클릭 방지 기능 구현 * refactor: 디바운스 시간을 설정할 수 있도록 수정 * feat: setOnClickListener함수도 디바운스 적용 가능하게 수정, 디폴트 디바운스타임 200ms로 변경 * fix: 함수 수정 * fix: 공모글작성, 공모글수정, 거래장소선택, 사진업로드, 사진추출, 검색 디바운스타임 800ms로 변경 * fix: 체크박스 디바운스 해제 * feat: playstore Deeplink 연결 (#545) * feat: 딥링크 설정을 위한 manifest 설정 * feat: 딥링크를 통해 offering detail로 이동하는 기능 구현 * feat: 딥링크로 참여하기를 누르는 경우 로그인 처리 * feat: 특정 기능 사용 시 확인/취소 다이얼로그 띄워주는 기능 구현 (#547) * feat: 로그아웃 alert 구현 * feat: 로그아웃 시 alert를 통해 확인받는 기능 구현 * feat: 공구 참여 시 확인받는 기능 구현 * feat: 공모 퇴장 시 확인받는 기능 구현 * style: ktlint format 적용 * feat: 릴리즈 버전 업데이트 (#550) * test: test fix (#555) * chore: release-* 에서 CD 작동하도록 수정 * chore: release-* 에서 CD 작동하도록 수정 * chore: release-* 에서 CD 작동하도록 수정 * feat: 웹 접근성 개선 (#575) * feat: 파비콘 추가 :) * feat: 공유 시 메타 정보가 보이도록 변경 * fix: AN 누락된 파일 추가 * fix: BE 누락된 파일 추가 * fix: workflows 누락된 파일 추가 --------- Co-authored-by: 채현 Co-authored-by: SCY Co-authored-by: fromitive <46563149+fromitive@users.noreply.github.com> Co-authored-by: Namyunsuk <84739562+Namyunsuk@users.noreply.github.com> Co-authored-by: masonkimseoul <87306418+masonkimseoul@users.noreply.github.com> Co-authored-by: alsong <138569524+songpink@users.noreply.github.com> Co-authored-by: chaehyun <80222352+chaehyuns@users.noreply.github.com> Co-authored-by: masonkimseoul Co-authored-by: fromitive Co-authored-by: Namyunsuk Co-authored-by: songpink --- .github/workflows/android.yml | 82 ++- .github/workflows/backend-dev-ci-cd.yml | 6 +- .github/workflows/backend-prod-ci-cd.yml | 24 +- android/app/build.gradle.kts | 157 ++--- .../chongdae/CommentRoomsFragmentTest.kt | 30 - android/app/src/main/AndroidManifest.xml | 16 +- .../java/com/zzang/chongdae/ChongdaeApp.kt | 86 +-- .../remote => auth}/api/AuthApiService.kt | 6 +- .../dto/request/AccessTokenRequest.kt | 2 +- .../dto/response}/MemberResponse.kt | 2 +- .../chongdae/auth/mapper/MemberMapper.kt | 11 + .../chongdae/{domain => auth}/model/Member.kt | 2 +- .../auth/repository/AuthRepository.kt | 11 + .../auth/repository/AuthRepositoryImpl.kt | 26 + .../auth/source/AuthRemoteDataSource.kt | 12 + .../auth/source/AuthRemoteDataSourceImpl.kt | 24 + .../datastore/UserPreferencesDataStore.kt | 70 ++ .../firebase}/FirebaseAnalyticsManager.kt | 2 +- .../util => common/handler}/DataError.kt | 2 +- .../zzang/chongdae/common/handler/Error.kt | 3 + .../{domain/util => common/handler}/Result.kt | 2 +- .../source/OfferingLocalDataSourceImpl.kt | 20 +- .../local/source/UserPreferencesDataStore.kt | 64 -- .../chongdae/data/mapper/CommentsMapper.kt | 31 - .../chongdae/data/mapper/MemberMapper.kt | 11 - .../data/remote/api/NetworkManager.kt | 6 +- .../data/remote/api/OfferingApiService.kt | 14 + .../dto/request/OfferingModifyRequest.kt | 19 + .../offering/OfferingDetailResponse.kt | 1 + .../offering/OfferingModifyResponse.kt | 22 + .../mapper/CommentCreatedAtMapper.kt | 2 +- .../mapper/CommentOfferingInfoMapper.kt | 2 +- .../mapper/CommentRoomResponseMapper.kt | 2 +- .../data/remote/mapper/CommentsMapper.kt | 14 + .../{ => remote}/mapper/CurrentCountMapper.kt | 2 +- .../data/{ => remote}/mapper/FilterMapper.kt | 2 +- .../mapper/LocalDateTimeMapper.kt | 2 +- .../mapper/MeetingsResponseMapper.kt | 2 +- .../mapper/OfferingConditionMapper.kt | 2 +- .../mapper/OfferingDetailResponseMapper.kt | 3 +- .../{ => remote}/mapper/OfferingMapper.kt | 2 +- .../remote/mapper/OfferingModifyMapper.kt | 41 ++ .../data/remote/mapper/OfferingWriteMapper.kt | 20 + .../mapper/ParticipationsResponseMapper.kt | 2 +- .../{ => remote}/mapper/ProductUrlMapper.kt | 2 +- .../mapper/participant/ParticipantsMapper.kt | 2 +- .../remote/source/AuthRemoteDataSourceImpl.kt | 21 - .../source/CommentRemoteDataSourceImpl.kt | 48 +- .../source/CommentRoomsDataSourceImpl.kt | 20 +- .../source/OfferingDetailDataSourceImpl.kt | 31 +- .../source/OfferingRemoteDataSourceImpl.kt | 82 ++- .../source/ParticipantRemoteDataSourceImpl.kt | 32 +- .../data/remote/util/CallApiHandler.kt | 4 +- .../data/remote/util/TokensCookieJar.kt | 2 +- .../data/repository/AuthRepositoryImpl.kt | 37 -- .../repository/CommentDetailRepositoryImpl.kt | 62 +- .../repository/CommentRoomsRepositoryImpl.kt | 24 +- .../OfferingDetailRepositoryImpl.kt | 42 +- .../data/repository/OfferingRepositoryImpl.kt | 120 ++-- .../repository/ParticipantRepositoryImpl.kt | 34 +- .../data/source/AuthRemoteDataSource.kt | 12 - .../data/source/CommentRoomsDataSource.kt | 4 +- .../data/source/OfferingDetailDataSource.kt | 6 +- .../source/ParticipantRemoteDataSource.kt | 4 +- .../source/comment/CommentRemoteDataSource.kt | 4 +- .../offering/OfferingRemoteDataSource.kt | 10 +- .../chongdae/di/annotations/AuthQualifier.kt | 15 + .../di/annotations/CommentDetailQualifier.kt | 15 + .../di/annotations/CommentRoomsQualifier.kt | 15 + .../di/annotations/DataStoreQualifier.kt | 7 + .../di/annotations/OfferingDetailQualifier.kt | 15 + .../di/annotations/OfferingQualifier.kt | 23 + .../di/annotations/ParticipantQualifier.kt | 15 + .../di/module/AuthDependencyModule.kt | 40 ++ .../module/CommentDetailDependencyModule.kt | 40 ++ .../di/module/CommentRoomsDependencyModule.kt | 40 ++ .../di/module/DataStoreDependencyModule.kt | 23 + .../di/module/OfferingDependencyModule.kt | 58 ++ .../module/OfferingDetailDependencyModule.kt | 40 ++ .../di/module/ParticipantDependencyModule.kt | 40 ++ .../chongdae/domain/model/OfferingDetail.kt | 1 + .../model/OfferingModifyDomainRequest.kt} | 4 +- .../model/OfferingModifyDomainResponse.kt | 20 + .../chongdae/domain/model/OfferingWrite.kt | 15 + .../domain/paging/OfferingPagingSource.kt | 6 +- .../domain/repository/AuthRepository.kt | 11 - .../repository/CommentDetailRepository.kt | 4 +- .../repository/CommentRoomsRepository.kt | 4 +- .../repository/OfferingDetailRepository.kt | 6 +- .../domain/repository/OfferingRepository.kt | 14 +- .../repository/ParticipantRepository.kt | 4 +- .../com/zzang/chongdae/domain/util/Error.kt | 3 - .../util/AccessTokenExpirationHandler.kt | 19 - .../presentation/util/BindingAdapters.kt | 11 + .../util/DebouncedClickListener.kt | 21 + .../presentation/view/MainActivity.kt | 40 ++ .../view/comment/CommentRoomsFragment.kt | 12 +- .../view/comment/CommentRoomsViewModel.kt | 74 +-- .../commentdetail/CommentDetailActivity.kt | 48 +- .../commentdetail/CommentDetailViewModel.kt | 458 +++++++------ .../view/common/OnAlertClickListener.kt | 7 + .../presentation/view/home/HomeFragment.kt | 41 +- .../view/home/OfferingViewModel.kt | 351 +++++----- .../presentation/view/login/LoginActivity.kt | 14 +- .../presentation/view/login/LoginViewModel.kt | 89 ++- .../view/mypage/MyPageFragment.kt | 29 +- .../view/mypage/MyPageViewModel.kt | 86 +-- .../offeringdetail/OfferingDetailFragment.kt | 96 ++- .../offeringdetail/OfferingDetailViewModel.kt | 324 +++++---- .../OnOfferingDeleteAlertClickListener.kt | 7 + .../OnOfferingModifyClickListener.kt | 5 + .../OnParticipationClickListener.kt | 2 +- .../view/offeringmodify/ModifyUIState.kt | 24 + .../OfferingModifyEssentialFragment.kt | 225 +++++++ .../OfferingModifyOptionalFragment.kt | 192 ++++++ .../offeringmodify/OfferingModifyViewModel.kt | 489 ++++++++++++++ .../write/OfferingWriteEssentialFragment.kt | 20 +- .../write/OfferingWriteOptionalFragment.kt | 16 +- .../view/write/OfferingWriteViewModel.kt | 620 +++++++++--------- ...r.kt => OnDateTimeButtonsClickListener.kt} | 2 +- .../res/layout/activity_comment_detail.xml | 16 +- .../src/main/res/layout/activity_login.xml | 2 +- .../app/src/main/res/layout/dialog_alert.xml | 71 ++ .../main/res/layout/dialog_date_picker.xml | 28 +- .../res/layout/dialog_date_time_picker.xml | 6 +- .../res/layout/dialog_delete_offering.xml | 71 ++ .../main/res/layout/dialog_update_status.xml | 4 +- .../app/src/main/res/layout/fragment_home.xml | 221 ++++--- .../src/main/res/layout/fragment_my_page.xml | 8 +- .../res/layout/fragment_offering_detail.xml | 12 +- .../fragment_offering_modify_essential.xml | 297 +++++++++ .../fragment_offering_modify_optional.xml | 285 ++++++++ .../fragment_offering_write_essential.xml | 8 +- .../fragment_offering_write_optional.xml | 13 +- .../layout/item_comment_room_participant.xml | 2 +- .../res/layout/item_comment_room_proposer.xml | 6 +- .../src/main/res/layout/item_my_comment.xml | 1 + .../app/src/main/res/layout/item_offering.xml | 10 +- .../res/navigation/bottom_menu_navigation.xml | 26 +- android/app/src/main/res/values/strings.xml | 19 +- android/app/src/main/res/values/style.xml | 7 +- .../view/comment/CommentRoomsViewModelTest.kt | 2 +- .../CommentDetailViewModelTest.kt | 2 +- .../view/home/OfferingViewModelTest.kt | 4 +- .../OfferingDetailViewModelTest.kt | 10 +- .../view/write/OfferingWriteViewModelTest.kt | 2 +- .../chongdae/repository/FakeAuthRepository.kt | 8 +- .../repository/FakeCommentDetailRepository.kt | 4 +- .../repository/FakeCommentRoomsRepository.kt | 4 +- .../FakeOfferingDetailRepository.kt | 10 +- .../repository/FakeOfferingRepository.kt | 16 +- .../repository/FakeParticipantRepository.kt | 4 +- .../com/zzang/chongdae/util/TestFixture.kt | 7 +- android/build.gradle.kts | 1 + android/gradle.properties | 4 + android/gradle/libs.versions.toml | 106 ++- backend/.gitignore | 3 + backend/build.gradle | 2 + backend/http/auth.http | 2 +- backend/http/comment.http | 3 + backend/http/offering.http | 3 + .../auth/config/AuthWebMvcConfig.java | 5 +- .../auth/exception/AuthErrorCode.java | 5 +- .../chongdae/auth/service/AuthService.java | 2 + .../auth/service/JwtTokenProvider.java | 34 +- .../comment/repository/CommentRepository.java | 2 +- .../repository/entity/CommentEntity.java | 3 +- .../comment/service/CommentService.java | 45 +- .../dto/CommentRoomAllResponseItem.java | 15 +- .../service/dto/CommentRoomInfoResponse.java | 12 + .../global/config/DataSourceAspect.java | 28 + .../global/config/DataSourceConfig.java | 54 ++ .../global/config/DataSourceRouter.java | 19 + .../global/config/WriterDatabase.java | 11 + .../controller/OfferingController.java | 20 + .../OfferingReadOnlyController.java | 49 ++ .../OfferingReadOnlyWebPageController.java | 29 + .../offering/domain/CommentRoomStatus.java | 8 +- .../offering/domain/OfferingPrice.java | 24 +- .../offering/domain/UpdatedOffering.java | 42 ++ .../offering/exception/OfferingErrorCode.java | 9 +- .../repository/OfferingRepository.java | 18 +- .../repository/entity/OfferingEntity.java | 26 +- .../offering/service/OfferingService.java | 57 +- .../service/OgTagProductImageExtractor.java | 1 - .../service/dto/OfferingDetailResponse.java | 2 + .../service/dto/OfferingMetaResponse.java | 18 + .../service/dto/OfferingSaveRequest.java | 1 - .../service/dto/OfferingUpdateRequest.java | 43 ++ .../service/dto/OfferingUpdateResponse.java | 46 ++ .../repository/OfferingMemberRepository.java | 10 + .../entity/OfferingMemberEntity.java | 3 +- .../service/OfferingMemberService.java | 7 +- .../scheduler/service/SchedulerService.java | 2 + .../src/main/resources/application-prod.yml | 13 +- .../src/main/resources/application.properties | 3 + backend/src/main/resources/application.yml | 16 +- backend/src/main/resources/static/favicon.ico | Bin 0 -> 15406 bytes .../resources/static/nickname/adjectives.txt | 2 +- .../main/resources/static/nickname/nouns.txt | 2 +- .../main/resources/static/nickname/nouns2.txt | 2 +- .../src/main/resources/templates/detail.html | 168 +++++ .../src/main/resources/templates/index.html | 349 ++++++++++ .../auth/integration/AuthIntegrationTest.java | 46 +- .../integration/CommentIntegrationTest.java | 6 +- .../comment/service/CommentServiceTest.java | 98 +++ .../chongdae/global/domain/MemberFixture.java | 12 + .../global/domain/OfferingFixture.java | 40 +- .../member/service/NicknameGeneratorTest.java | 2 +- .../integration/OfferingIntegrationTest.java | 388 ++++++++++- .../OfferingReadOnlyIntegrationTest.java | 186 ++++++ .../offering/service/OfferingServiceTest.java | 487 ++++++++++++-- .../resources/static/nickname/adjectives.txt | 2 +- .../test/resources/static/nickname/nouns.txt | 2 +- .../test/resources/static/nickname/nouns2.txt | 2 +- 215 files changed, 6991 insertions(+), 2060 deletions(-) delete mode 100644 android/app/src/androidTest/java/com/zzang/chongdae/CommentRoomsFragmentTest.kt rename android/app/src/main/java/com/zzang/chongdae/{data/remote => auth}/api/AuthApiService.kt (63%) rename android/app/src/main/java/com/zzang/chongdae/{data/remote => auth}/dto/request/AccessTokenRequest.kt (78%) rename android/app/src/main/java/com/zzang/chongdae/{data/remote/dto/response/auth => auth/dto/response}/MemberResponse.kt (79%) create mode 100644 android/app/src/main/java/com/zzang/chongdae/auth/mapper/MemberMapper.kt rename android/app/src/main/java/com/zzang/chongdae/{domain => auth}/model/Member.kt (64%) create mode 100644 android/app/src/main/java/com/zzang/chongdae/auth/repository/AuthRepository.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/auth/repository/AuthRepositoryImpl.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSource.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSourceImpl.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/common/datastore/UserPreferencesDataStore.kt rename android/app/src/main/java/com/zzang/chongdae/{presentation/util => common/firebase}/FirebaseAnalyticsManager.kt (95%) rename android/app/src/main/java/com/zzang/chongdae/{domain/util => common/handler}/DataError.kt (87%) create mode 100644 android/app/src/main/java/com/zzang/chongdae/common/handler/Error.kt rename android/app/src/main/java/com/zzang/chongdae/{domain/util => common/handler}/Result.kt (93%) delete mode 100644 android/app/src/main/java/com/zzang/chongdae/data/local/source/UserPreferencesDataStore.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentsMapper.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/data/mapper/MemberMapper.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/data/remote/dto/request/OfferingModifyRequest.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingModifyResponse.kt rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/CommentCreatedAtMapper.kt (87%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/CommentOfferingInfoMapper.kt (90%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/CommentRoomResponseMapper.kt (90%) create mode 100644 android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentsMapper.kt rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/CurrentCountMapper.kt (68%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/FilterMapper.kt (95%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/LocalDateTimeMapper.kt (90%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/MeetingsResponseMapper.kt (89%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/OfferingConditionMapper.kt (91%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/OfferingDetailResponseMapper.kt (91%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/OfferingMapper.kt (92%) create mode 100644 android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingModifyMapper.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingWriteMapper.kt rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/ParticipationsResponseMapper.kt (87%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/ProductUrlMapper.kt (90%) rename android/app/src/main/java/com/zzang/chongdae/data/{ => remote}/mapper/participant/ParticipantsMapper.kt (95%) delete mode 100644 android/app/src/main/java/com/zzang/chongdae/data/remote/source/AuthRemoteDataSourceImpl.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/data/repository/AuthRepositoryImpl.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/data/source/AuthRemoteDataSource.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/AuthQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentDetailQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentRoomsQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/DataStoreQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingDetailQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/annotations/ParticipantQualifier.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/AuthDependencyModule.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/CommentDetailDependencyModule.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/CommentRoomsDependencyModule.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/DataStoreDependencyModule.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDependencyModule.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDetailDependencyModule.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/di/module/ParticipantDependencyModule.kt rename android/app/src/main/java/com/zzang/chongdae/{presentation/view/write/OfferingWriteUiModel.kt => domain/model/OfferingModifyDomainRequest.kt} (79%) create mode 100644 android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainResponse.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingWrite.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/domain/repository/AuthRepository.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/domain/util/Error.kt delete mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/util/AccessTokenExpirationHandler.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/util/DebouncedClickListener.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/common/OnAlertClickListener.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingDeleteAlertClickListener.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingModifyClickListener.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/ModifyUIState.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyEssentialFragment.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyOptionalFragment.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyViewModel.kt rename android/app/src/main/java/com/zzang/chongdae/presentation/view/write/{OnOfferingWriteClickListener.kt => OnDateTimeButtonsClickListener.kt} (75%) create mode 100644 android/app/src/main/res/layout/dialog_alert.xml create mode 100644 android/app/src/main/res/layout/dialog_delete_offering.xml create mode 100644 android/app/src/main/res/layout/fragment_offering_modify_essential.xml create mode 100644 android/app/src/main/res/layout/fragment_offering_modify_optional.xml create mode 100644 backend/src/main/java/com/zzang/chongdae/global/config/DataSourceAspect.java create mode 100644 backend/src/main/java/com/zzang/chongdae/global/config/DataSourceConfig.java create mode 100644 backend/src/main/java/com/zzang/chongdae/global/config/DataSourceRouter.java create mode 100644 backend/src/main/java/com/zzang/chongdae/global/config/WriterDatabase.java create mode 100644 backend/src/main/java/com/zzang/chongdae/offering/controller/OfferingReadOnlyController.java create mode 100644 backend/src/main/java/com/zzang/chongdae/offering/controller/OfferingReadOnlyWebPageController.java create mode 100644 backend/src/main/java/com/zzang/chongdae/offering/domain/UpdatedOffering.java create mode 100644 backend/src/main/java/com/zzang/chongdae/offering/service/dto/OfferingMetaResponse.java create mode 100644 backend/src/main/java/com/zzang/chongdae/offering/service/dto/OfferingUpdateRequest.java create mode 100644 backend/src/main/java/com/zzang/chongdae/offering/service/dto/OfferingUpdateResponse.java create mode 100644 backend/src/main/resources/static/favicon.ico create mode 100644 backend/src/main/resources/templates/detail.html create mode 100644 backend/src/main/resources/templates/index.html create mode 100644 backend/src/test/java/com/zzang/chongdae/comment/service/CommentServiceTest.java create mode 100644 backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingReadOnlyIntegrationTest.java diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4529bfbec..37ce2af8e 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,14 +1,10 @@ -name: Android CI +name: Android CI CD on: - push: - branches: [ "develop-AN" ] - paths: - - 'android/**' pull_request: - branches: [ "develop-AN" ] - paths: - - 'android/**' + branches: + - "develop" + - "release*" defaults: run: @@ -89,7 +85,7 @@ jobs: echo "native_app_key=$NATIVE_APP_KEY" >> ./local.properties - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -106,3 +102,71 @@ jobs: - name: Run Unit Test run: ./gradlew test + + deploy: + runs-on: ubuntu-latest + needs: build_and_test + if: startsWith(github.event.pull_request.base.ref, 'release-') + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Create google-services.json + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: | + echo "$GOOGLE_SERVICES_JSON" > app/google-services.json + + - name: Create service_account.json + id: createServiceAccount + run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > app/service_account.json + + - name: Set up environment variable for BuildConfig + env: + BASE_URL: ${{ secrets.BASE_URL }} + TOKEN: ${{ secrets.TOKEN }} + NATIVE_APP_KEY: ${{ secrets.NATIVE_APP_KEY }} + run: | + echo "base_url=$BASE_URL" >> ./local.properties + echo "token=$TOKEN" >> ./local.properties + echo "native_app_key=$NATIVE_APP_KEY" >> ./local.properties + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/buildSrc/**/*.kt') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build release AAB + run: ./gradlew bundleRelease + + - name: Sign AAB + id: sign + uses: r0adkll/sign-android-release@v1 + with: + releaseDirectory: ./android/app/build/outputs/bundle/release + output: ./android/build/release/signed + signingKeyBase64: ${{ secrets.ENCODED_KEYSTORE }} + alias: ${{ secrets.AN_ALIAS }} + keyStorePassword: ${{ secrets.AN_KEYSTORE_PASSWORD }} + keyPassword: ${{ secrets.AN_KEY_PASSWORD }} + + - name: Upload AAB to Google Play + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} + packageName: com.zzang.chongdae + releaseFiles: ./android/app/build/outputs/bundle/release/app-release.aab + track: "총대마켓 - 비공개 테스트" diff --git a/.github/workflows/backend-dev-ci-cd.yml b/.github/workflows/backend-dev-ci-cd.yml index a1f703ad1..255ca93dd 100644 --- a/.github/workflows/backend-dev-ci-cd.yml +++ b/.github/workflows/backend-dev-ci-cd.yml @@ -2,14 +2,14 @@ name: Backend Dev CI/CD Workflow on: push: - branches: [ "develop-BE" ] + branches: [ "develop" ] paths: - "backend/**" - ".github/workflows/backend-dev-ci-cd.yml" - "Dockerfile" # pull_request: - # branches: [ "develop-BE" ] - # paths: + # branches: [ "develop" ] + # paths: # - "backend/**" # - ".github/workflows/backend-dev-ci-cd.yml" # - "Dockerfile" diff --git a/.github/workflows/backend-prod-ci-cd.yml b/.github/workflows/backend-prod-ci-cd.yml index 1b72e3b17..8c605ad52 100644 --- a/.github/workflows/backend-prod-ci-cd.yml +++ b/.github/workflows/backend-prod-ci-cd.yml @@ -8,7 +8,7 @@ on: - ".github/workflows/backend-prod-ci-cd.yml" - "Dockerfile" # pull_request: - # branches: [ "develop-BE" ] + # branches: [ "develop" ] # paths: # - "backend/**" # - ".github/workflows/backend-prod-ci-cd.yml" @@ -57,16 +57,30 @@ jobs: docker tag ${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }} ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} docker push ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} - deploy: + deploy-a: needs: build-and-test - runs-on: [ self-hosted, prod ] + runs-on: prod-a steps: - - name: Pull Image And Restart Container + - name: Pull Image And Restart Container on Production A run: | docker login -u ${{ secrets.BE_DOCKERHUB_USERNAME }} -p ${{ secrets.BE_DOCKERHUB_PASSWORD }} docker stop ${{ secrets.BE_DOCKER_CONTAINER_NAME }} | true docker rm ${{ secrets.BE_DOCKER_CONTAINER_NAME }} | true docker image prune -a -f docker pull ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} - docker run --name ${{ secrets.BE_DOCKER_CONTAINER_NAME }} --network nginx_network -d -v /logs:/logs -e SPRING_PROFILES_ACTIVE=prod ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} + docker run --name ${{ secrets.BE_DOCKER_CONTAINER_NAME }} -d -v /logs:/logs -p 80:8080 -e SPRING_PROFILES_ACTIVE=prod ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} + + deploy-b: + needs: deploy-a + runs-on: prod-b + + steps: + - name: Pull Image And Restart Container on Production B + run: | + docker login -u ${{ secrets.BE_DOCKERHUB_USERNAME }} -p ${{ secrets.BE_DOCKERHUB_PASSWORD }} + docker stop ${{ secrets.BE_DOCKER_CONTAINER_NAME }} | true + docker rm ${{ secrets.BE_DOCKER_CONTAINER_NAME }} | true + docker image prune -a -f + docker pull ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} + docker run --name ${{ secrets.BE_DOCKER_CONTAINER_NAME }} -d -v /logs:/logs -p 80:8080 -e SPRING_PROFILES_ACTIVE=prod ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_PROD }}:${GITHUB_SHA::7} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index add5aac42..b7b8cc5c9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,6 +9,7 @@ plugins { id("com.google.gms.google-services") kotlin("plugin.serialization") version "2.0.0" id("com.google.firebase.crashlytics") + id("com.google.dagger.hilt.android") } android { @@ -28,8 +29,8 @@ android { applicationId = "com.zzang.chongdae" minSdk = 26 targetSdk = 34 - versionCode = 2 - versionName = "1.1.0" + versionCode = 6 + versionName = "1.1.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder" @@ -49,11 +50,7 @@ android { buildTypes { debug { - isMinifyEnabled = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) + isMinifyEnabled = false } release { isMinifyEnabled = true @@ -86,89 +83,93 @@ android { } dependencies { - val navigationVersion = "2.7.7" - val fragmentVersion = "1.8.1" - implementation("androidx.core:core-ktx:1.10.1") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.10.0") - implementation("androidx.activity:activity-ktx:1.8.2") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.test.ext:junit-ktx:1.1.5") - testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") - testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("io.kotest:kotest-runner-junit5:5.8.0") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("androidx.test:runner:1.4.0") - androidTestImplementation("org.junit.jupiter:junit-jupiter:5.10.2") - androidTestImplementation("org.assertj:assertj-core:3.25.3") - androidTestImplementation("io.kotest:kotest-runner-junit5:5.8.0") - androidTestImplementation("de.mannodermaus.junit5:android-test-core:1.3.0") - androidTestRuntimeOnly("de.mannodermaus.junit5:android-test-runner:1.3.0") - // Testing Navigation - androidTestImplementation("androidx.navigation:navigation-testing:$navigationVersion") - - implementation("androidx.room:room-runtime:2.6.1") - kapt("androidx.room:room-compiler:2.6.1") - implementation("androidx.room:room-ktx:2.6.1") - implementation("com.google.code.gson:gson:2.8.8") - - implementation("com.github.bumptech.glide:glide:4.12.0") - kapt("com.github.bumptech.glide:compiler:4.12.0") - testImplementation("androidx.arch.core:core-testing:2.1.0") - implementation("com.squareup.okhttp3:mockwebserver:4.12.0") - - implementation("com.squareup.retrofit2:retrofit:2.11.0") - implementation("com.squareup.retrofit2:converter-gson:2.11.0") - - implementation("androidx.room:room-ktx:2.6.1") - - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") - implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") - - kapt("com.github.bumptech.glide:compiler:4.13.2") - - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.activity:activity-ktx:1.9.0") - implementation("androidx.fragment:fragment-ktx:1.7.0") - implementation("androidx.core:core-ktx:1.10.1") implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.fragment) + implementation(libs.androidx.constraintlayout) + + // Test + implementation(libs.androidx.junit) + testImplementation(libs.junit.jupiter) + testImplementation(libs.assertj.core) + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.core.testing) + + // Android Test + androidTestImplementation(libs.junit.jupiter) + androidTestImplementation(libs.assertj.core) + androidTestImplementation(libs.kotest.runner.junit5) + androidTestImplementation(libs.mannodermaus.test.core) + androidTestImplementation(libs.mannodermaus.test.runner) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.test.runner) + + // Espresso 및 관련 + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.espresso.contrib) + + // UI Test: Fragment Scenario + debugImplementation(libs.androidx.fragment.testing) + androidTestImplementation(libs.androidx.fragment.testing) + + // DataStore + implementation(libs.androidx.datastore.preferences) + + // Lifecycle + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + + // Room + implementation(libs.androidx.room.runtime) + kapt(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + + // json + implementation(libs.kotlinx.serialization.json) + + // Glide + implementation(libs.glide) + kapt(libs.glide.compiler) + + // Retrofit + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) + implementation(libs.retrofit.kotlinx.serialization) // Navigation - implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion") - implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion") - - // UI Test - Fragment Scenario - debugImplementation("androidx.fragment:fragment-testing-manifest:$fragmentVersion") - androidTestImplementation("androidx.fragment:fragment-testing:$fragmentVersion") - - // Espresso - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("androidx.test:runner:1.4.0") - - // Espresso RecyclerView Actions - androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0") + implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.ui) + androidTestImplementation(libs.androidx.navigation.testing) // Pagination - implementation("androidx.paging:paging-runtime-ktx:3.3.0") + implementation(libs.androidx.paging.runtime) // WebView - implementation("androidx.webkit:webkit:1.9.0") + implementation(libs.androidx.webkit) // Firebase - implementation(platform("com.google.firebase:firebase-bom:33.1.2")) - implementation("com.google.firebase:firebase-analytics") + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + implementation(libs.firebase.crashlytics) // 카카오 로그인 - implementation("com.kakao.sdk:v2-all:2.20.3") + implementation(libs.kakao.sdk) - // data store - implementation("androidx.datastore:datastore-preferences:1.0.0") + // Mockk + implementation(libs.mockwebserver) + testImplementation(libs.mockk) - implementation("com.google.firebase:firebase-crashlytics") + // Swipe Refresh Layout + implementation(libs.androidx.swiperefreshlayout) + + // Hilt + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) +} - // mockk - testImplementation("io.mockk:mockk:1.13.10") +kapt { + correctErrorTypes = true } diff --git a/android/app/src/androidTest/java/com/zzang/chongdae/CommentRoomsFragmentTest.kt b/android/app/src/androidTest/java/com/zzang/chongdae/CommentRoomsFragmentTest.kt deleted file mode 100644 index 82e05c116..000000000 --- a/android/app/src/androidTest/java/com/zzang/chongdae/CommentRoomsFragmentTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.zzang.chongdae - -import androidx.fragment.app.testing.FragmentScenario -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.zzang.chongdae.presentation.view.comment.CommentRoomsFragment -import org.junit.Before -import org.junit.Test -import org.junit.jupiter.api.DisplayName -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class CommentRoomsFragmentTest { - private lateinit var scenario: FragmentScenario - - @Before - fun setUp() { - scenario = FragmentScenario.launchInContainer(CommentRoomsFragment::class.java) - } - - @Test - @DisplayName("댓글방 목록으로 이동하면 채팅이라는 텍스트뷰가 보여야 한다") - fun commentRoomTest1() { - // then - onView(withId(R.id.tv_comment_text)).check(matches(isDisplayed())) - } -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 236f2f386..d411e8557 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,8 +36,20 @@ + android:exported="true" + android:windowSoftInputMode="stateAlwaysHidden|adjustPan"> + + + + + + + + + + + suspend fun saveRefresh(): Result +} diff --git a/android/app/src/main/java/com/zzang/chongdae/auth/repository/AuthRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/auth/repository/AuthRepositoryImpl.kt new file mode 100644 index 000000000..9977c7a8a --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/auth/repository/AuthRepositoryImpl.kt @@ -0,0 +1,26 @@ +package com.zzang.chongdae.auth.repository + +import com.zzang.chongdae.auth.dto.request.AccessTokenRequest +import com.zzang.chongdae.auth.mapper.toDomain +import com.zzang.chongdae.auth.model.Member +import com.zzang.chongdae.auth.source.AuthRemoteDataSource +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthDataSourceQualifier +import javax.inject.Inject + +class AuthRepositoryImpl + @Inject + constructor( + @AuthDataSourceQualifier private val authRemoteDataSource: AuthRemoteDataSource, + ) : AuthRepository { + override suspend fun saveLogin(accessToken: String): Result { + return authRemoteDataSource.saveLogin( + accessTokenRequest = AccessTokenRequest(accessToken), + ).map { it.toDomain() } + } + + override suspend fun saveRefresh(): Result { + return authRemoteDataSource.saveRefresh() + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSource.kt new file mode 100644 index 000000000..8bdcc7403 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSource.kt @@ -0,0 +1,12 @@ +package com.zzang.chongdae.auth.source + +import com.zzang.chongdae.auth.dto.request.AccessTokenRequest +import com.zzang.chongdae.auth.dto.response.MemberResponse +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result + +interface AuthRemoteDataSource { + suspend fun saveLogin(accessTokenRequest: AccessTokenRequest): Result + + suspend fun saveRefresh(): Result +} diff --git a/android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSourceImpl.kt new file mode 100644 index 000000000..fd97158dd --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/auth/source/AuthRemoteDataSourceImpl.kt @@ -0,0 +1,24 @@ +package com.zzang.chongdae.auth.source + +import com.zzang.chongdae.auth.api.AuthApiService +import com.zzang.chongdae.auth.dto.request.AccessTokenRequest +import com.zzang.chongdae.auth.dto.response.MemberResponse +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.data.remote.util.safeApiCall +import com.zzang.chongdae.di.annotations.AuthApiServiceQualifier +import javax.inject.Inject + +class AuthRemoteDataSourceImpl + @Inject + constructor( + @AuthApiServiceQualifier private val service: AuthApiService, + ) : AuthRemoteDataSource { + override suspend fun saveLogin(accessTokenRequest: AccessTokenRequest): Result { + return safeApiCall { service.postLogin(accessTokenRequest) } + } + + override suspend fun saveRefresh(): Result { + return safeApiCall { service.postRefresh() } + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/common/datastore/UserPreferencesDataStore.kt b/android/app/src/main/java/com/zzang/chongdae/common/datastore/UserPreferencesDataStore.kt new file mode 100644 index 000000000..0d257498f --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/common/datastore/UserPreferencesDataStore.kt @@ -0,0 +1,70 @@ +package com.zzang.chongdae.common.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import com.zzang.chongdae.di.annotations.DataStoreQualifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class UserPreferencesDataStore + @Inject + constructor( + @DataStoreQualifier private val dataStore: DataStore, + ) { + val memberIdFlow: Flow = + dataStore.data.map { preferences -> + preferences[MEMBER_ID_KEY] + } + + val nickNameFlow: Flow = + dataStore.data.map { preferences -> + preferences[NICKNAME_KEY] + } + + val accessTokenFlow: Flow = + dataStore.data.map { preferences -> + preferences[ACCESS_TOKEN_KEY] + } + + val refreshTokenFlow: Flow = + dataStore.data.map { preferences -> + preferences[REFRESH_TOKEN_KEY] + } + + suspend fun saveMember( + memberId: Long, + nickName: String, + ) { + dataStore.edit { preferences -> + preferences[MEMBER_ID_KEY] = memberId + preferences[NICKNAME_KEY] = nickName + } + } + + suspend fun saveTokens( + accessToken: String, + refreshToken: String, + ) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN_KEY] = accessToken + preferences[REFRESH_TOKEN_KEY] = refreshToken + } + } + + suspend fun removeAllData() { + dataStore.edit { preferences -> + preferences.clear() + } + } + + companion object { + val MEMBER_ID_KEY = longPreferencesKey("member_id_key") + val NICKNAME_KEY = stringPreferencesKey("nickname_key") + val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token_key") + val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token_key") + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/util/FirebaseAnalyticsManager.kt b/android/app/src/main/java/com/zzang/chongdae/common/firebase/FirebaseAnalyticsManager.kt similarity index 95% rename from android/app/src/main/java/com/zzang/chongdae/presentation/util/FirebaseAnalyticsManager.kt rename to android/app/src/main/java/com/zzang/chongdae/common/firebase/FirebaseAnalyticsManager.kt index 11334b627..29fee0962 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/util/FirebaseAnalyticsManager.kt +++ b/android/app/src/main/java/com/zzang/chongdae/common/firebase/FirebaseAnalyticsManager.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.presentation.util +package com.zzang.chongdae.common.firebase import android.os.Bundle import com.google.firebase.analytics.FirebaseAnalytics diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/util/DataError.kt b/android/app/src/main/java/com/zzang/chongdae/common/handler/DataError.kt similarity index 87% rename from android/app/src/main/java/com/zzang/chongdae/domain/util/DataError.kt rename to android/app/src/main/java/com/zzang/chongdae/common/handler/DataError.kt index 6c3ec920d..6e6935729 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/util/DataError.kt +++ b/android/app/src/main/java/com/zzang/chongdae/common/handler/DataError.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.domain.util +package com.zzang.chongdae.common.handler sealed interface DataError : Error { enum class Network : DataError { diff --git a/android/app/src/main/java/com/zzang/chongdae/common/handler/Error.kt b/android/app/src/main/java/com/zzang/chongdae/common/handler/Error.kt new file mode 100644 index 000000000..c0ab2f70c --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/common/handler/Error.kt @@ -0,0 +1,3 @@ +package com.zzang.chongdae.common.handler + +sealed interface Error diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/util/Result.kt b/android/app/src/main/java/com/zzang/chongdae/common/handler/Result.kt similarity index 93% rename from android/app/src/main/java/com/zzang/chongdae/domain/util/Result.kt rename to android/app/src/main/java/com/zzang/chongdae/common/handler/Result.kt index 1d994e79c..1d69cd1d5 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/util/Result.kt +++ b/android/app/src/main/java/com/zzang/chongdae/common/handler/Result.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.domain.util +package com.zzang.chongdae.common.handler typealias RootError = Error diff --git a/android/app/src/main/java/com/zzang/chongdae/data/local/source/OfferingLocalDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/local/source/OfferingLocalDataSourceImpl.kt index 0c0c4e189..d7b545f7e 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/local/source/OfferingLocalDataSourceImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/local/source/OfferingLocalDataSourceImpl.kt @@ -3,13 +3,19 @@ package com.zzang.chongdae.data.local.source import com.zzang.chongdae.data.local.dao.OfferingDao import com.zzang.chongdae.data.local.model.OfferingEntity import com.zzang.chongdae.data.source.offering.OfferingLocalDataSource +import com.zzang.chongdae.di.annotations.OfferingDaoQualifier +import javax.inject.Inject -class OfferingLocalDataSourceImpl(private val offeringDao: OfferingDao) : OfferingLocalDataSource { - override suspend fun insertOfferings(offerings: List) { - offeringDao.insertAll(offerings) - } +class OfferingLocalDataSourceImpl + @Inject + constructor( + @OfferingDaoQualifier private val offeringDao: OfferingDao, + ) : OfferingLocalDataSource { + override suspend fun insertOfferings(offerings: List) { + offeringDao.insertAll(offerings) + } - override suspend fun insertOffering(offering: OfferingEntity) { - offeringDao.insertOffering(offering) + override suspend fun insertOffering(offering: OfferingEntity) { + offeringDao.insertOffering(offering) + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/local/source/UserPreferencesDataStore.kt b/android/app/src/main/java/com/zzang/chongdae/data/local/source/UserPreferencesDataStore.kt deleted file mode 100644 index 1f257360d..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/data/local/source/UserPreferencesDataStore.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.zzang.chongdae.data.local.source - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class UserPreferencesDataStore(private val dataStore: DataStore) { - val memberIdFlow: Flow = - dataStore.data.map { preferences -> - preferences[MEMBER_ID_KEY] - } - - val nickNameFlow: Flow = - dataStore.data.map { preferences -> - preferences[NICKNAME_KEY] - } - - val accessTokenFlow: Flow = - dataStore.data.map { preferences -> - preferences[ACCESS_TOKEN_KEY] - } - - val refreshTokenFlow: Flow = - dataStore.data.map { preferences -> - preferences[REFRESH_TOKEN_KEY] - } - - suspend fun saveMember( - memberId: Long, - nickName: String, - ) { - dataStore.edit { preferences -> - preferences[MEMBER_ID_KEY] = memberId - preferences[NICKNAME_KEY] = nickName - } - } - - suspend fun saveTokens( - accessToken: String, - refreshToken: String, - ) { - dataStore.edit { preferences -> - preferences[ACCESS_TOKEN_KEY] = accessToken - preferences[REFRESH_TOKEN_KEY] = refreshToken - } - } - - suspend fun removeAllData() { - dataStore.edit { preferences -> - preferences.clear() - } - } - - companion object { - val MEMBER_ID_KEY = longPreferencesKey("member_id_key") - val NICKNAME_KEY = stringPreferencesKey("nickname_key") - val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token_key") - val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token_key") - } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentsMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentsMapper.kt deleted file mode 100644 index 0d8f09e26..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentsMapper.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.zzang.chongdae.data.mapper - -import com.zzang.chongdae.data.local.model.CommentEntity -import com.zzang.chongdae.data.remote.dto.response.comment.CommentResponse -import com.zzang.chongdae.domain.model.Comment - -fun CommentResponse.toDomain(): Comment { - return Comment( - content = this.content, - commentCreatedAt = this.commentCreatedAtResponse.toDomain(), - isMine = this.isMine, - isProposer = this.isProposer, - nickname = this.nickname, - ) -} - -fun mapToCommentEntity( - offeringId: Long, - commentResponse: CommentResponse, -): CommentEntity { - return CommentEntity( - offeringId = offeringId, - commentId = commentResponse.commentId, - content = commentResponse.content, - isMine = commentResponse.isMine, - isProposer = commentResponse.isProposer, - nickname = commentResponse.nickname, - commentCreatedAtDate = commentResponse.commentCreatedAtResponse.date, - commentCreatedAtTime = commentResponse.commentCreatedAtResponse.time, - ) -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/MemberMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/mapper/MemberMapper.kt deleted file mode 100644 index 1e34a613e..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/MemberMapper.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.zzang.chongdae.data.mapper - -import com.zzang.chongdae.data.remote.dto.response.auth.MemberResponse -import com.zzang.chongdae.domain.model.Member - -fun MemberResponse.toDomain(): Member { - return Member( - memberId = this.memberId, - nickName = this.nickname, - ) -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/api/NetworkManager.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/api/NetworkManager.kt index 30098821e..671e82596 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/api/NetworkManager.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/api/NetworkManager.kt @@ -4,7 +4,8 @@ import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFact import com.zzang.chongdae.BuildConfig import com.zzang.chongdae.ChongdaeApp import com.zzang.chongdae.ChongdaeApp.Companion.dataStore -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.auth.api.AuthApiService +import com.zzang.chongdae.common.datastore.UserPreferencesDataStore import com.zzang.chongdae.data.remote.util.TokensCookieJar import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType @@ -21,7 +22,8 @@ object NetworkManager { } private fun getRetrofit(): Retrofit { - val userDataStore = UserPreferencesDataStore(ChongdaeApp.chongdaeApplicationContext.dataStore) + val userDataStore = + UserPreferencesDataStore(ChongdaeApp.chongdaeAppContext.dataStore) if (instance == null) { val contentType = "application/json".toMediaType() instance = diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/api/OfferingApiService.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/api/OfferingApiService.kt index 8107fd35f..57225068b 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/api/OfferingApiService.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/api/OfferingApiService.kt @@ -1,5 +1,6 @@ package com.zzang.chongdae.data.remote.api +import com.zzang.chongdae.data.remote.dto.request.OfferingModifyRequest import com.zzang.chongdae.data.remote.dto.request.OfferingWriteRequest import com.zzang.chongdae.data.remote.dto.request.ProductUrlRequest import com.zzang.chongdae.data.remote.dto.response.offering.FiltersResponse @@ -11,8 +12,10 @@ import com.zzang.chongdae.data.remote.dto.response.offering.RemoteOffering import okhttp3.MultipartBody import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Multipart +import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Part import retrofit2.http.Path @@ -60,4 +63,15 @@ interface OfferingApiService { suspend fun postProductImageS3( @Part image: MultipartBody.Part, ): Response + + @PATCH("/offerings/{offering-id}") + suspend fun patchOffering( + @Path("offering-id") offeringId: Long, + @Body offeringModifyRequest: OfferingModifyRequest, + ): Response + + @DELETE("/offerings/{offering-id}") + suspend fun deleteOffering( + @Path("offering-id") offeringId: Long, + ): Response } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/request/OfferingModifyRequest.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/request/OfferingModifyRequest.kt new file mode 100644 index 000000000..bc0ee9525 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/request/OfferingModifyRequest.kt @@ -0,0 +1,19 @@ +package com.zzang.chongdae.data.remote.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OfferingModifyRequest( + @SerialName("title") val title: String, + @SerialName("productUrl") val productUrl: String?, + @SerialName("thumbnailUrl") val thumbnailUrl: String?, + @SerialName("totalCount") val totalCount: Int, + @SerialName("totalPrice") val totalPrice: Int, + @SerialName("originPrice") val originPrice: Int?, + @SerialName("meetingAddress") val meetingAddress: String, + @SerialName("meetingAddressDong") val meetingAddressDong: String?, + @SerialName("meetingAddressDetail") val meetingAddressDetail: String, + @SerialName("meetingDate") val meetingDate: String, + @SerialName("description") val description: String, +) diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingDetailResponse.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingDetailResponse.kt index efcde9653..e4db2180d 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingDetailResponse.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingDetailResponse.kt @@ -17,6 +17,7 @@ data class OfferingDetailResponse( @SerialName("thumbnailUrl") val thumbnailUrl: String?, @SerialName("dividedPrice") val dividedPrice: Int, @SerialName("totalPrice") val totalPrice: Int, + @SerialName("originPrice") val originPrice: Int?, @SerialName("status") val condition: RemoteOfferingStatus, @SerialName("isProposer") val isProposer: Boolean, @SerialName("nickname") val nickname: String, diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingModifyResponse.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingModifyResponse.kt new file mode 100644 index 000000000..7021e8dfb --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/dto/response/offering/OfferingModifyResponse.kt @@ -0,0 +1,22 @@ +package com.zzang.chongdae.data.remote.dto.response.offering + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OfferingModifyResponse( + @SerialName("id") val id: Long, + @SerialName("title") val title: String, + @SerialName("productUrl") val productUrl: String?, + @SerialName("meetingAddress") val meetingAddress: String, + @SerialName("meetingAddressDetail") val meetingAddressDetail: String, + @SerialName("description") val description: String, + @SerialName("meetingDate") val meetingDate: String, + @SerialName("currentCount") val currentCount: Int, + @SerialName("totalCount") val totalCount: Int, + @SerialName("thumbnailUrl") val thumbnailUrl: String?, + @SerialName("dividedPrice") val dividedPrice: Int, + @SerialName("totalPrice") val totalPrice: Int, + @SerialName("status") val condition: RemoteOfferingStatus, + @SerialName("nickname") val nickname: String, +) diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentCreatedAtMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentCreatedAtMapper.kt similarity index 87% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentCreatedAtMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentCreatedAtMapper.kt index 987c3bb08..2a1077966 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentCreatedAtMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentCreatedAtMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.comment.CommentCreatedAtResponse import com.zzang.chongdae.domain.model.CommentCreatedAt diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentOfferingInfoMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentOfferingInfoMapper.kt similarity index 90% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentOfferingInfoMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentOfferingInfoMapper.kt index 9240ac815..eee2fdb3f 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentOfferingInfoMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentOfferingInfoMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.comment.CommentOfferingInfoResponse import com.zzang.chongdae.domain.model.CommentOfferingInfo diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentRoomResponseMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentRoomResponseMapper.kt similarity index 90% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentRoomResponseMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentRoomResponseMapper.kt index d38acfaed..8a31e38b8 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CommentRoomResponseMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentRoomResponseMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.commentroom.CommentRoomResponse import com.zzang.chongdae.domain.model.CommentRoom diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentsMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentsMapper.kt new file mode 100644 index 000000000..3a694d3ca --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CommentsMapper.kt @@ -0,0 +1,14 @@ +package com.zzang.chongdae.data.remote.mapper + +import com.zzang.chongdae.data.remote.dto.response.comment.CommentResponse +import com.zzang.chongdae.domain.model.Comment + +fun CommentResponse.toDomain(): Comment { + return Comment( + content = this.content, + commentCreatedAt = this.commentCreatedAtResponse.toDomain(), + isMine = this.isMine, + isProposer = this.isProposer, + nickname = this.nickname, + ) +} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CurrentCountMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CurrentCountMapper.kt similarity index 68% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/CurrentCountMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CurrentCountMapper.kt index 9d7cb36e0..317041114 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/CurrentCountMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/CurrentCountMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.domain.model.CurrentCount diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/FilterMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/FilterMapper.kt similarity index 95% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/FilterMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/FilterMapper.kt index 1209d172a..c7ed5e973 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/FilterMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/FilterMapper.kt @@ -1,6 +1,6 @@ @file:Suppress("UNUSED_EXPRESSION") -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.offering.RemoteFilter import com.zzang.chongdae.data.remote.dto.response.offering.RemoteFilterName diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/LocalDateTimeMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/LocalDateTimeMapper.kt similarity index 90% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/LocalDateTimeMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/LocalDateTimeMapper.kt index 6b4bbe711..c138019a2 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/LocalDateTimeMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/LocalDateTimeMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import java.time.LocalDate import java.time.LocalDateTime diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/MeetingsResponseMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/MeetingsResponseMapper.kt similarity index 89% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/MeetingsResponseMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/MeetingsResponseMapper.kt index 49e75195f..a0a79e7a1 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/MeetingsResponseMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/MeetingsResponseMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.offering.MeetingsResponse import com.zzang.chongdae.domain.model.Meetings diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingConditionMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingConditionMapper.kt similarity index 91% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingConditionMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingConditionMapper.kt index 1231fc25f..e5e332971 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingConditionMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingConditionMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.offering.RemoteOfferingStatus import com.zzang.chongdae.domain.model.OfferingCondition diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingDetailResponseMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingDetailResponseMapper.kt similarity index 91% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingDetailResponseMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingDetailResponseMapper.kt index e40e3440e..2b77a7337 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingDetailResponseMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingDetailResponseMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.offering.OfferingDetailResponse import com.zzang.chongdae.domain.model.OfferingDetail @@ -13,6 +13,7 @@ fun OfferingDetailResponse.toDomain() = dividedPrice = this.dividedPrice, thumbnailUrl = this.thumbnailUrl, totalPrice = this.totalPrice, + originPrice = this.originPrice, meetingDate = this.meetingDate.toLocalDateTime(), currentCount = this.currentCount.toCurrentCount(), totalCount = this.totalCount, diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingMapper.kt similarity index 92% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingMapper.kt index 09818f45b..fd4696e89 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/OfferingMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.offering.RemoteOffering import com.zzang.chongdae.domain.model.Offering diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingModifyMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingModifyMapper.kt new file mode 100644 index 000000000..2a1f7ca5f --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingModifyMapper.kt @@ -0,0 +1,41 @@ +package com.zzang.chongdae.data.remote.mapper + +import com.zzang.chongdae.data.remote.dto.request.OfferingModifyRequest +import com.zzang.chongdae.data.remote.dto.response.offering.OfferingModifyResponse +import com.zzang.chongdae.domain.model.OfferingModifyDomainRequest +import com.zzang.chongdae.domain.model.OfferingModifyDomainResponse + +fun OfferingModifyDomainRequest.toRequest(): OfferingModifyRequest { + return OfferingModifyRequest( + title = this.title, + productUrl = this.productUrl, + thumbnailUrl = this.thumbnailUrl, + totalCount = this.totalCount, + totalPrice = this.totalPrice, + originPrice = this.originPrice, + meetingAddress = this.meetingAddress, + meetingAddressDong = this.meetingAddressDong, + meetingAddressDetail = this.meetingAddressDetail, + meetingDate = this.meetingDate, + description = this.description, + ) +} + +fun OfferingModifyResponse.toDomain(): OfferingModifyDomainResponse { + return OfferingModifyDomainResponse( + id = this.id, + title = this.title, + productUrl = this.productUrl, + meetingAddress = this.meetingAddress, + meetingAddressDetail = this.meetingAddressDetail, + description = this.description, + meetingDate = this.meetingDate.toLocalDateTime(), + currentCount = this.currentCount.toCurrentCount(), + totalCount = this.totalCount, + thumbnailUrl = this.thumbnailUrl, + dividedPrice = this.dividedPrice, + totalPrice = this.totalPrice, + condition = this.condition.toDomain(), + nickname = this.nickname, + ) +} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingWriteMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingWriteMapper.kt new file mode 100644 index 000000000..e8a4c7821 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/OfferingWriteMapper.kt @@ -0,0 +1,20 @@ +package com.zzang.chongdae.data.remote.mapper + +import com.zzang.chongdae.data.remote.dto.request.OfferingWriteRequest +import com.zzang.chongdae.domain.model.OfferingWrite + +fun OfferingWrite.toRequest(): OfferingWriteRequest { + return OfferingWriteRequest( + title = this.title, + productUrl = this.productUrl, + thumbnailUrl = this.thumbnailUrl, + totalCount = this.totalCount, + totalPrice = this.totalPrice, + originPrice = this.originPrice, + meetingAddress = this.meetingAddress, + meetingAddressDong = this.meetingAddressDong, + meetingAddressDetail = this.meetingAddressDetail, + meetingDate = this.meetingDate, + description = this.description, + ) +} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/ParticipationsResponseMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/ParticipationsResponseMapper.kt similarity index 87% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/ParticipationsResponseMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/ParticipationsResponseMapper.kt index a4bc9365a..4dd506e95 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/ParticipationsResponseMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/ParticipationsResponseMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.response.offering.ParticipationResponse import com.zzang.chongdae.domain.model.Participation diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/ProductUrlMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/ProductUrlMapper.kt similarity index 90% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/ProductUrlMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/ProductUrlMapper.kt index 655e20aaf..df8b53ae1 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/ProductUrlMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/ProductUrlMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper +package com.zzang.chongdae.data.remote.mapper import com.zzang.chongdae.data.remote.dto.request.ProductUrlRequest import com.zzang.chongdae.data.remote.dto.response.offering.ProductUrlResponse diff --git a/android/app/src/main/java/com/zzang/chongdae/data/mapper/participant/ParticipantsMapper.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/participant/ParticipantsMapper.kt similarity index 95% rename from android/app/src/main/java/com/zzang/chongdae/data/mapper/participant/ParticipantsMapper.kt rename to android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/participant/ParticipantsMapper.kt index 45901377b..dfac3fa6f 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/mapper/participant/ParticipantsMapper.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/mapper/participant/ParticipantsMapper.kt @@ -1,4 +1,4 @@ -package com.zzang.chongdae.data.mapper.participant +package com.zzang.chongdae.data.remote.mapper.participant import com.zzang.chongdae.data.remote.dto.response.participants.ParticipantsResponse import com.zzang.chongdae.data.remote.dto.response.participants.RemoteCount diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/AuthRemoteDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/AuthRemoteDataSourceImpl.kt deleted file mode 100644 index fa01a5385..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/AuthRemoteDataSourceImpl.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.zzang.chongdae.data.remote.source - -import com.zzang.chongdae.data.remote.api.AuthApiService -import com.zzang.chongdae.data.remote.dto.request.AccessTokenRequest -import com.zzang.chongdae.data.remote.dto.response.auth.MemberResponse -import com.zzang.chongdae.data.remote.util.safeApiCall -import com.zzang.chongdae.data.source.AuthRemoteDataSource -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result - -class AuthRemoteDataSourceImpl( - private val service: AuthApiService, -) : AuthRemoteDataSource { - override suspend fun saveLogin(accessTokenRequest: AccessTokenRequest): Result { - return safeApiCall { service.postLogin(accessTokenRequest) } - } - - override suspend fun saveRefresh(): Result { - return safeApiCall { service.postRefresh() } - } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRemoteDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRemoteDataSourceImpl.kt index 7977fb2fd..88df30413 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRemoteDataSourceImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRemoteDataSourceImpl.kt @@ -1,5 +1,7 @@ package com.zzang.chongdae.data.remote.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.api.CommentApiService import com.zzang.chongdae.data.remote.dto.request.CommentRequest import com.zzang.chongdae.data.remote.dto.response.comment.CommentOfferingInfoResponse @@ -7,29 +9,31 @@ import com.zzang.chongdae.data.remote.dto.response.comment.CommentsResponse import com.zzang.chongdae.data.remote.dto.response.comment.UpdatedStatusResponse import com.zzang.chongdae.data.remote.util.safeApiCall import com.zzang.chongdae.data.source.comment.CommentRemoteDataSource -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.di.annotations.CommentDetailApiServiceQualifier +import javax.inject.Inject -class CommentRemoteDataSourceImpl( - private val service: CommentApiService, -) : CommentRemoteDataSource { - override suspend fun saveComment(commentRequest: CommentRequest): Result = - safeApiCall { - service.postComment(commentRequest) - } +class CommentRemoteDataSourceImpl + @Inject + constructor( + @CommentDetailApiServiceQualifier private val service: CommentApiService, + ) : CommentRemoteDataSource { + override suspend fun saveComment(commentRequest: CommentRequest): Result = + safeApiCall { + service.postComment(commentRequest) + } - override suspend fun fetchComments(offeringId: Long): Result = - safeApiCall { - service.getComments(offeringId) - } + override suspend fun fetchComments(offeringId: Long): Result = + safeApiCall { + service.getComments(offeringId) + } - override suspend fun fetchCommentOfferingInfo(offeringId: Long): Result = - safeApiCall { - service.getCommentOfferingInfo(offeringId) - } + override suspend fun fetchCommentOfferingInfo(offeringId: Long): Result = + safeApiCall { + service.getCommentOfferingInfo(offeringId) + } - override suspend fun updateOfferingStatus(offeringId: Long): Result = - safeApiCall { - service.patchOfferingStatus(offeringId) - } -} + override suspend fun updateOfferingStatus(offeringId: Long): Result = + safeApiCall { + service.patchOfferingStatus(offeringId) + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRoomsDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRoomsDataSourceImpl.kt index 87c72925c..2d5002e6f 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRoomsDataSourceImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/CommentRoomsDataSourceImpl.kt @@ -1,16 +1,20 @@ package com.zzang.chongdae.data.remote.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.api.CommentApiService import com.zzang.chongdae.data.remote.dto.response.commentroom.CommentRoomsResponse import com.zzang.chongdae.data.remote.util.safeApiCall import com.zzang.chongdae.data.source.CommentRoomsDataSource -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.di.annotations.CommentRoomsApiServiceQualifier +import javax.inject.Inject -class CommentRoomsDataSourceImpl( - private val commentApiService: CommentApiService, -) : CommentRoomsDataSource { - override suspend fun fetchCommentRooms(): Result { - return safeApiCall { commentApiService.getCommentRooms() } +class CommentRoomsDataSourceImpl + @Inject + constructor( + @CommentRoomsApiServiceQualifier private val commentApiService: CommentApiService, + ) : CommentRoomsDataSource { + override suspend fun fetchCommentRooms(): Result { + return safeApiCall { commentApiService.getCommentRooms() } + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingDetailDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingDetailDataSourceImpl.kt index 38f79e7d1..2c66f5e8d 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingDetailDataSourceImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingDetailDataSourceImpl.kt @@ -1,21 +1,30 @@ package com.zzang.chongdae.data.remote.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.api.OfferingApiService import com.zzang.chongdae.data.remote.api.ParticipationApiService import com.zzang.chongdae.data.remote.dto.request.ParticipationRequest import com.zzang.chongdae.data.remote.dto.response.offering.OfferingDetailResponse import com.zzang.chongdae.data.remote.util.safeApiCall import com.zzang.chongdae.data.source.OfferingDetailDataSource -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.di.annotations.OfferingApiServiceQualifier +import com.zzang.chongdae.di.annotations.ParticipantApiServiceQualifier +import javax.inject.Inject -class OfferingDetailDataSourceImpl( - private val offeringApiService: OfferingApiService, - private val participationApiService: ParticipationApiService, -) : OfferingDetailDataSource { - override suspend fun fetchOfferingDetail(offeringId: Long): Result = - safeApiCall { offeringApiService.getOfferingDetail(offeringId) } +class OfferingDetailDataSourceImpl + @Inject + constructor( + @OfferingApiServiceQualifier private val offeringApiService: OfferingApiService, + @ParticipantApiServiceQualifier private val participationApiService: ParticipationApiService, + ) : OfferingDetailDataSource { + override suspend fun fetchOfferingDetail(offeringId: Long): Result = + safeApiCall { offeringApiService.getOfferingDetail(offeringId) } - override suspend fun saveParticipation(participationRequest: ParticipationRequest): Result = - safeApiCall { participationApiService.postParticipations(participationRequest) } -} + override suspend fun saveParticipation(participationRequest: ParticipationRequest): Result = + safeApiCall { participationApiService.postParticipations(participationRequest) } + + override suspend fun deleteOffering(offeringId: Long): Result { + return safeApiCall { offeringApiService.deleteOffering(offeringId) } + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingRemoteDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingRemoteDataSourceImpl.kt index f3994d1a2..df2dc3782 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingRemoteDataSourceImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/OfferingRemoteDataSourceImpl.kt @@ -1,48 +1,60 @@ package com.zzang.chongdae.data.remote.source -import com.zzang.chongdae.data.mapper.toProductUrlRequest +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.api.OfferingApiService +import com.zzang.chongdae.data.remote.dto.request.OfferingModifyRequest import com.zzang.chongdae.data.remote.dto.request.OfferingWriteRequest import com.zzang.chongdae.data.remote.dto.response.offering.FiltersResponse import com.zzang.chongdae.data.remote.dto.response.offering.MeetingsResponse import com.zzang.chongdae.data.remote.dto.response.offering.OfferingsResponse import com.zzang.chongdae.data.remote.dto.response.offering.ProductUrlResponse import com.zzang.chongdae.data.remote.dto.response.offering.RemoteOffering +import com.zzang.chongdae.data.remote.mapper.toProductUrlRequest import com.zzang.chongdae.data.remote.util.safeApiCall import com.zzang.chongdae.data.source.offering.OfferingRemoteDataSource -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.di.annotations.OfferingApiServiceQualifier import okhttp3.MultipartBody - -class OfferingRemoteDataSourceImpl( - private val service: OfferingApiService, -) : OfferingRemoteDataSource { - override suspend fun fetchOffering(offeringId: Long): Result = - safeApiCall { service.getOffering(offeringId) } - - override suspend fun fetchOfferings( - filter: String?, - search: String?, - lastOfferingId: Long?, - pageSize: Int?, - ): Result = safeApiCall { service.getOfferings(filter, search, lastOfferingId, pageSize) } - - override suspend fun saveOffering(offeringWriteRequest: OfferingWriteRequest): Result = - safeApiCall { service.postOfferingWrite((offeringWriteRequest)) } - - override suspend fun saveProductImageOg(productUrl: String): Result = - safeApiCall { service.postProductImageOg((productUrl.toProductUrlRequest())) } - - override suspend fun saveProductImageS3(image: MultipartBody.Part): Result = - safeApiCall { service.postProductImageS3(image) } - - override suspend fun fetchFilters(): Result = safeApiCall { service.getFilters() } - - override suspend fun fetchMeetings(offeringId: Long): Result = - safeApiCall { service.getMeetings(offeringId) } - - companion object { - private const val ERROR_PREFIX = "에러 발생: " - private const val ERROR_NULL_MESSAGE = "null" +import javax.inject.Inject + +class OfferingRemoteDataSourceImpl + @Inject + constructor( + @OfferingApiServiceQualifier private val service: OfferingApiService, + ) : OfferingRemoteDataSource { + override suspend fun fetchOffering(offeringId: Long): Result = + safeApiCall { service.getOffering(offeringId) } + + override suspend fun fetchOfferings( + filter: String?, + search: String?, + lastOfferingId: Long?, + pageSize: Int?, + ): Result = safeApiCall { service.getOfferings(filter, search, lastOfferingId, pageSize) } + + override suspend fun saveOffering(offeringWriteRequest: OfferingWriteRequest): Result = + safeApiCall { service.postOfferingWrite((offeringWriteRequest)) } + + override suspend fun saveProductImageOg(productUrl: String): Result = + safeApiCall { service.postProductImageOg((productUrl.toProductUrlRequest())) } + + override suspend fun saveProductImageS3(image: MultipartBody.Part): Result = + safeApiCall { service.postProductImageS3(image) } + + override suspend fun fetchFilters(): Result = safeApiCall { service.getFilters() } + + override suspend fun fetchMeetings(offeringId: Long): Result = + safeApiCall { service.getMeetings(offeringId) } + + override suspend fun patchOffering( + offeringId: Long, + offeringModifyRequest: OfferingModifyRequest, + ): Result { + return safeApiCall { service.patchOffering(offeringId, offeringModifyRequest) } + } + + companion object { + private const val ERROR_PREFIX = "에러 발생: " + private const val ERROR_NULL_MESSAGE = "null" + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/ParticipantRemoteDataSourceImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/ParticipantRemoteDataSourceImpl.kt index a847e76df..250222a5c 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/source/ParticipantRemoteDataSourceImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/source/ParticipantRemoteDataSourceImpl.kt @@ -1,22 +1,26 @@ package com.zzang.chongdae.data.remote.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.api.ParticipationApiService import com.zzang.chongdae.data.remote.dto.response.participants.ParticipantsResponse import com.zzang.chongdae.data.remote.util.safeApiCall import com.zzang.chongdae.data.source.ParticipantRemoteDataSource -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.di.annotations.ParticipantApiServiceQualifier +import javax.inject.Inject -class ParticipantRemoteDataSourceImpl( - private val service: ParticipationApiService, -) : ParticipantRemoteDataSource { - override suspend fun fetchParticipants(offeringId: Long): Result = - safeApiCall { - service.getParticipants(offeringId) - } +class ParticipantRemoteDataSourceImpl + @Inject + constructor( + @ParticipantApiServiceQualifier private val service: ParticipationApiService, + ) : ParticipantRemoteDataSource { + override suspend fun fetchParticipants(offeringId: Long): Result = + safeApiCall { + service.getParticipants(offeringId) + } - override suspend fun deleteParticipations(offeringId: Long): Result = - safeApiCall { - service.deleteParticipations(offeringId) - } -} + override suspend fun deleteParticipations(offeringId: Long): Result = + safeApiCall { + service.deleteParticipations(offeringId) + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/util/CallApiHandler.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/util/CallApiHandler.kt index aabc7932f..148630f71 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/util/CallApiHandler.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/util/CallApiHandler.kt @@ -1,7 +1,7 @@ package com.zzang.chongdae.data.remote.util -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import retrofit2.HttpException import retrofit2.Response import java.io.IOException diff --git a/android/app/src/main/java/com/zzang/chongdae/data/remote/util/TokensCookieJar.kt b/android/app/src/main/java/com/zzang/chongdae/data/remote/util/TokensCookieJar.kt index 07b839fbb..4c94fb81d 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/remote/util/TokensCookieJar.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/remote/util/TokensCookieJar.kt @@ -1,7 +1,7 @@ package com.zzang.chongdae.data.remote.util import com.zzang.chongdae.BuildConfig -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.common.datastore.UserPreferencesDataStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first diff --git a/android/app/src/main/java/com/zzang/chongdae/data/repository/AuthRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/repository/AuthRepositoryImpl.kt deleted file mode 100644 index cdf833974..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/data/repository/AuthRepositoryImpl.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.zzang.chongdae.data.repository - -import com.zzang.chongdae.data.mapper.toDomain -import com.zzang.chongdae.data.remote.dto.request.AccessTokenRequest -import com.zzang.chongdae.data.source.AuthRemoteDataSource -import com.zzang.chongdae.domain.model.Member -import com.zzang.chongdae.domain.repository.AuthRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result - -class AuthRepositoryImpl( - private val authRemoteDataSource: AuthRemoteDataSource, -) : AuthRepository { - override suspend fun saveLogin(accessToken: String): Result { - return authRemoteDataSource.saveLogin( - accessTokenRequest = AccessTokenRequest(accessToken), - ).map { it.toDomain() } - } - - override suspend fun saveRefresh(): Result { - return when (val result = authRemoteDataSource.saveRefresh()) { - is Result.Error -> { - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - return Result.Error(result.msg, DataError.Network.FAIL_REFRESH) - } - - else -> { - return Result.Error(result.msg, DataError.Network.UNAUTHORIZED) - } - } - } - - is Result.Success -> result - } - } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentDetailRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentDetailRepositoryImpl.kt index bb4f8d5a2..6d01ae361 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentDetailRepositoryImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentDetailRepositoryImpl.kt @@ -1,40 +1,46 @@ package com.zzang.chongdae.data.repository -import com.zzang.chongdae.data.mapper.toDomain +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.dto.request.CommentRequest +import com.zzang.chongdae.data.remote.mapper.toDomain import com.zzang.chongdae.data.source.comment.CommentRemoteDataSource +import com.zzang.chongdae.di.annotations.CommentDetailDataSourceQualifier import com.zzang.chongdae.domain.model.Comment import com.zzang.chongdae.domain.model.CommentOfferingInfo import com.zzang.chongdae.domain.repository.CommentDetailRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import javax.inject.Inject -class CommentDetailRepositoryImpl( - private val commentRemoteDataSource: CommentRemoteDataSource, -) : CommentDetailRepository { - override suspend fun saveComment( - offeringId: Long, - comment: String, - ): Result { - return commentRemoteDataSource.saveComment( - CommentRequest(offeringId, comment), - ).map { Unit } - } +class CommentDetailRepositoryImpl + @Inject + constructor( + @CommentDetailDataSourceQualifier private val commentRemoteDataSource: CommentRemoteDataSource, + ) : CommentDetailRepository { + override suspend fun saveComment( + offeringId: Long, + comment: String, + ): Result { + return commentRemoteDataSource.saveComment( + CommentRequest(offeringId, comment), + ).map { Unit } + } - override suspend fun fetchComments(offeringId: Long): Result, DataError.Network> { - return commentRemoteDataSource.fetchComments(offeringId) - .map { response -> - response.commentsResponse.map { it.toDomain() } - } - } + override suspend fun fetchComments(offeringId: Long): Result, DataError.Network> { + return commentRemoteDataSource.fetchComments(offeringId) + .map { response -> + response.commentsResponse.map { it.toDomain() } + } + } - override suspend fun fetchCommentOfferingInfo(offeringId: Long): Result { - return commentRemoteDataSource.fetchCommentOfferingInfo(offeringId) - .map { it.toDomain() } - } + override suspend fun fetchCommentOfferingInfo(offeringId: Long): Result { + return commentRemoteDataSource.fetchCommentOfferingInfo(offeringId) + .map { response -> + response.toDomain() + } + } - override suspend fun updateOfferingStatus(offeringId: Long): Result { - return commentRemoteDataSource.updateOfferingStatus(offeringId) - .map { Unit } + override suspend fun updateOfferingStatus(offeringId: Long): Result { + return commentRemoteDataSource.updateOfferingStatus(offeringId) + .map { Unit } + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentRoomsRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentRoomsRepositoryImpl.kt index 924cf8544..d8ef5b867 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentRoomsRepositoryImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/repository/CommentRoomsRepositoryImpl.kt @@ -1,18 +1,22 @@ package com.zzang.chongdae.data.repository -import com.zzang.chongdae.data.mapper.toDomain +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.data.remote.mapper.toDomain import com.zzang.chongdae.data.source.CommentRoomsDataSource +import com.zzang.chongdae.di.annotations.CommentRoomsDataSourceQualifier import com.zzang.chongdae.domain.model.CommentRoom import com.zzang.chongdae.domain.repository.CommentRoomsRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import javax.inject.Inject -class CommentRoomsRepositoryImpl( - private val commentRoomsDataSource: CommentRoomsDataSource, -) : CommentRoomsRepository { - override suspend fun fetchCommentRooms(): Result, DataError.Network> { - return commentRoomsDataSource.fetchCommentRooms().map { - it.commentRoom.map { it.toDomain() } +class CommentRoomsRepositoryImpl + @Inject + constructor( + @CommentRoomsDataSourceQualifier private val commentRoomsDataSource: CommentRoomsDataSource, + ) : CommentRoomsRepository { + override suspend fun fetchCommentRooms(): Result, DataError.Network> { + return commentRoomsDataSource.fetchCommentRooms().map { + it.commentRoom.map { it.toDomain() } + } } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingDetailRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingDetailRepositoryImpl.kt index b206f444c..b1ababab5 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingDetailRepositoryImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingDetailRepositoryImpl.kt @@ -1,25 +1,33 @@ package com.zzang.chongdae.data.repository -import com.zzang.chongdae.data.mapper.toDomain +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.dto.request.ParticipationRequest +import com.zzang.chongdae.data.remote.mapper.toDomain import com.zzang.chongdae.data.source.OfferingDetailDataSource +import com.zzang.chongdae.di.annotations.OfferingDetailDataSourceQualifier import com.zzang.chongdae.domain.model.OfferingDetail import com.zzang.chongdae.domain.repository.OfferingDetailRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import javax.inject.Inject -class OfferingDetailRepositoryImpl( - private val offeringDetailDataSource: OfferingDetailDataSource, -) : OfferingDetailRepository { - override suspend fun fetchOfferingDetail(offeringId: Long): Result = - offeringDetailDataSource.fetchOfferingDetail( - offeringId = offeringId, - ).map { - it.toDomain() - } +class OfferingDetailRepositoryImpl + @Inject + constructor( + @OfferingDetailDataSourceQualifier private val offeringDetailDataSource: OfferingDetailDataSource, + ) : OfferingDetailRepository { + override suspend fun fetchOfferingDetail(offeringId: Long): Result = + offeringDetailDataSource.fetchOfferingDetail( + offeringId = offeringId, + ).map { + it.toDomain() + } + + override suspend fun saveParticipation(offeringId: Long): Result = + offeringDetailDataSource.saveParticipation( + participationRequest = ParticipationRequest(offeringId), + ) - override suspend fun saveParticipation(offeringId: Long): Result = - offeringDetailDataSource.saveParticipation( - participationRequest = ParticipationRequest(offeringId), - ) -} + override suspend fun deleteOffering(offeringId: Long): Result { + return offeringDetailDataSource.deleteOffering(offeringId) + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingRepositoryImpl.kt index 20793d043..bd0f2d3bf 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingRepositoryImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/repository/OfferingRepositoryImpl.kt @@ -1,80 +1,84 @@ package com.zzang.chongdae.data.repository -import com.zzang.chongdae.data.mapper.toDomain -import com.zzang.chongdae.data.remote.dto.request.OfferingWriteRequest +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.data.remote.mapper.toDomain +import com.zzang.chongdae.data.remote.mapper.toRequest import com.zzang.chongdae.data.source.offering.OfferingLocalDataSource import com.zzang.chongdae.data.source.offering.OfferingRemoteDataSource +import com.zzang.chongdae.di.annotations.OfferingLocalDataSourceQualifier +import com.zzang.chongdae.di.annotations.OfferingRemoteDataSourceQualifier import com.zzang.chongdae.domain.model.Filter import com.zzang.chongdae.domain.model.Meetings import com.zzang.chongdae.domain.model.Offering +import com.zzang.chongdae.domain.model.OfferingModifyDomainRequest +import com.zzang.chongdae.domain.model.OfferingWrite import com.zzang.chongdae.domain.model.ProductUrl import com.zzang.chongdae.domain.repository.OfferingRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result -import com.zzang.chongdae.presentation.view.write.OfferingWriteUiModel import okhttp3.MultipartBody +import javax.inject.Inject -class OfferingRepositoryImpl( - private val offeringLocalDataSource: OfferingLocalDataSource, - private val offeringRemoteDataSource: OfferingRemoteDataSource, -) : OfferingRepository { - override suspend fun fetchOffering(offeringId: Long): Result = - offeringRemoteDataSource.fetchOffering(offeringId = offeringId).map { - it.toDomain() - } - - override suspend fun fetchOfferings( - filter: String?, - search: String?, - lastOfferingId: Long?, - pageSize: Int?, - ): Result, DataError.Network> { - return offeringRemoteDataSource.fetchOfferings(filter, search, lastOfferingId, pageSize) - .map { - it.offerings.map { it.toDomain() } +class OfferingRepositoryImpl + @Inject + constructor( + @OfferingLocalDataSourceQualifier private val offeringLocalDataSource: OfferingLocalDataSource, + @OfferingRemoteDataSourceQualifier private val offeringRemoteDataSource: OfferingRemoteDataSource, + ) : OfferingRepository { + override suspend fun fetchOffering(offeringId: Long): Result = + offeringRemoteDataSource.fetchOffering(offeringId = offeringId).map { + it.toDomain() } - } - override suspend fun saveOffering(uiModel: OfferingWriteUiModel): Result { - return offeringRemoteDataSource.saveOffering( - offeringWriteRequest = - OfferingWriteRequest( - title = uiModel.title, - productUrl = uiModel.productUrl, - thumbnailUrl = uiModel.thumbnailUrl, - totalCount = uiModel.totalCount, - totalPrice = uiModel.totalPrice, - originPrice = uiModel.originPrice, - meetingAddress = uiModel.meetingAddress, - meetingAddressDong = uiModel.meetingAddressDong, - meetingAddressDetail = uiModel.meetingAddressDetail, - meetingDate = uiModel.meetingDate, - description = uiModel.description, - ), - ) - } + override suspend fun fetchOfferings( + filter: String?, + search: String?, + lastOfferingId: Long?, + pageSize: Int?, + ): Result, DataError.Network> { + return offeringRemoteDataSource.fetchOfferings(filter, search, lastOfferingId, pageSize) + .map { + it.offerings.map { it.toDomain() } + } + } - override suspend fun saveProductImageOg(productUrl: String): Result { - return offeringRemoteDataSource.saveProductImageOg(productUrl).map { - it.toDomain() + override suspend fun saveOffering(offeringWrite: OfferingWrite): Result { + return offeringRemoteDataSource.saveOffering( + offeringWriteRequest = offeringWrite.toRequest(), + ) } - } - override suspend fun saveProductImageS3(image: MultipartBody.Part): Result { - return offeringRemoteDataSource.saveProductImageS3(image).map { - it.toDomain() + override suspend fun saveProductImageOg(productUrl: String): Result { + return offeringRemoteDataSource.saveProductImageOg(productUrl).map { + it.toDomain() + } } - } - override suspend fun fetchFilters(): Result, DataError.Network> { - return offeringRemoteDataSource.fetchFilters().map { - it.filters.map { it.toDomain() } + override suspend fun saveProductImageS3(image: MultipartBody.Part): Result { + return offeringRemoteDataSource.saveProductImageS3(image).map { + it.toDomain() + } + } + + override suspend fun fetchFilters(): Result, DataError.Network> { + return offeringRemoteDataSource.fetchFilters().map { + it.filters.map { it.toDomain() } + } } - } - override suspend fun fetchMeetings(offeringId: Long): Result { - return offeringRemoteDataSource.fetchMeetings(offeringId).map { - it.toDomain() + override suspend fun fetchMeetings(offeringId: Long): Result { + return offeringRemoteDataSource.fetchMeetings(offeringId).map { + it.toDomain() + } } + + override suspend fun patchOffering( + offeringId: Long, + offeringModifyDomainRequest: OfferingModifyDomainRequest, + ): Result = + offeringRemoteDataSource.patchOffering( + offeringId, + offeringModifyDomainRequest.toRequest(), + ).map { + it // .toDomain() + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/repository/ParticipantRepositoryImpl.kt b/android/app/src/main/java/com/zzang/chongdae/data/repository/ParticipantRepositoryImpl.kt index 6abae202b..7f4d48b57 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/repository/ParticipantRepositoryImpl.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/repository/ParticipantRepositoryImpl.kt @@ -1,22 +1,26 @@ package com.zzang.chongdae.data.repository -import com.zzang.chongdae.data.mapper.participant.toDomain +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.data.remote.mapper.participant.toDomain import com.zzang.chongdae.data.source.ParticipantRemoteDataSource +import com.zzang.chongdae.di.annotations.ParticipantDataSourceQualifier import com.zzang.chongdae.domain.model.participant.Participants import com.zzang.chongdae.domain.repository.ParticipantRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import javax.inject.Inject -class ParticipantRepositoryImpl( - private val participantRemoteDataSource: ParticipantRemoteDataSource, -) : ParticipantRepository { - override suspend fun fetchParticipants(offeringId: Long): Result = - participantRemoteDataSource.fetchParticipants( - offeringId, - ).map { response -> - response.toDomain() - } +class ParticipantRepositoryImpl + @Inject + constructor( + @ParticipantDataSourceQualifier private val participantRemoteDataSource: ParticipantRemoteDataSource, + ) : ParticipantRepository { + override suspend fun fetchParticipants(offeringId: Long): Result = + participantRemoteDataSource.fetchParticipants( + offeringId, + ).map { response -> + response.toDomain() + } - override suspend fun deleteParticipations(offeringId: Long): Result = - participantRemoteDataSource.deleteParticipations(offeringId) -} + override suspend fun deleteParticipations(offeringId: Long): Result = + participantRemoteDataSource.deleteParticipations(offeringId) + } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/source/AuthRemoteDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/data/source/AuthRemoteDataSource.kt deleted file mode 100644 index d101edff6..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/data/source/AuthRemoteDataSource.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.zzang.chongdae.data.source - -import com.zzang.chongdae.data.remote.dto.request.AccessTokenRequest -import com.zzang.chongdae.data.remote.dto.response.auth.MemberResponse -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result - -interface AuthRemoteDataSource { - suspend fun saveLogin(accessTokenRequest: AccessTokenRequest): Result - - suspend fun saveRefresh(): Result -} diff --git a/android/app/src/main/java/com/zzang/chongdae/data/source/CommentRoomsDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/data/source/CommentRoomsDataSource.kt index f133264ed..c48283cd7 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/source/CommentRoomsDataSource.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/source/CommentRoomsDataSource.kt @@ -1,8 +1,8 @@ package com.zzang.chongdae.data.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.dto.response.commentroom.CommentRoomsResponse -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface CommentRoomsDataSource { suspend fun fetchCommentRooms(): Result diff --git a/android/app/src/main/java/com/zzang/chongdae/data/source/OfferingDetailDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/data/source/OfferingDetailDataSource.kt index b32806232..e6cdb92ed 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/source/OfferingDetailDataSource.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/source/OfferingDetailDataSource.kt @@ -1,12 +1,14 @@ package com.zzang.chongdae.data.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.dto.request.ParticipationRequest import com.zzang.chongdae.data.remote.dto.response.offering.OfferingDetailResponse -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface OfferingDetailDataSource { suspend fun fetchOfferingDetail(offeringId: Long): Result suspend fun saveParticipation(participationRequest: ParticipationRequest): Result + + suspend fun deleteOffering(offeringId: Long): Result } diff --git a/android/app/src/main/java/com/zzang/chongdae/data/source/ParticipantRemoteDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/data/source/ParticipantRemoteDataSource.kt index b53e3c663..588a4a7c8 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/source/ParticipantRemoteDataSource.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/source/ParticipantRemoteDataSource.kt @@ -1,8 +1,8 @@ package com.zzang.chongdae.data.source +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.dto.response.participants.ParticipantsResponse -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface ParticipantRemoteDataSource { suspend fun fetchParticipants(offeringId: Long): Result diff --git a/android/app/src/main/java/com/zzang/chongdae/data/source/comment/CommentRemoteDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/data/source/comment/CommentRemoteDataSource.kt index 3ce476747..f3b96a0ae 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/source/comment/CommentRemoteDataSource.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/source/comment/CommentRemoteDataSource.kt @@ -1,11 +1,11 @@ package com.zzang.chongdae.data.source.comment +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.data.remote.dto.request.CommentRequest import com.zzang.chongdae.data.remote.dto.response.comment.CommentOfferingInfoResponse import com.zzang.chongdae.data.remote.dto.response.comment.CommentsResponse import com.zzang.chongdae.data.remote.dto.response.comment.UpdatedStatusResponse -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface CommentRemoteDataSource { suspend fun saveComment(commentRequest: CommentRequest): Result diff --git a/android/app/src/main/java/com/zzang/chongdae/data/source/offering/OfferingRemoteDataSource.kt b/android/app/src/main/java/com/zzang/chongdae/data/source/offering/OfferingRemoteDataSource.kt index 4f290c056..7524554c7 100644 --- a/android/app/src/main/java/com/zzang/chongdae/data/source/offering/OfferingRemoteDataSource.kt +++ b/android/app/src/main/java/com/zzang/chongdae/data/source/offering/OfferingRemoteDataSource.kt @@ -1,13 +1,14 @@ package com.zzang.chongdae.data.source.offering +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.data.remote.dto.request.OfferingModifyRequest import com.zzang.chongdae.data.remote.dto.request.OfferingWriteRequest import com.zzang.chongdae.data.remote.dto.response.offering.FiltersResponse import com.zzang.chongdae.data.remote.dto.response.offering.MeetingsResponse import com.zzang.chongdae.data.remote.dto.response.offering.OfferingsResponse import com.zzang.chongdae.data.remote.dto.response.offering.ProductUrlResponse import com.zzang.chongdae.data.remote.dto.response.offering.RemoteOffering -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result import okhttp3.MultipartBody interface OfferingRemoteDataSource { @@ -29,4 +30,9 @@ interface OfferingRemoteDataSource { suspend fun fetchFilters(): Result suspend fun fetchMeetings(offeringId: Long): Result + + suspend fun patchOffering( + offeringId: Long, + offeringModifyRequest: OfferingModifyRequest, + ): Result } diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/AuthQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/AuthQualifier.kt new file mode 100644 index 000000000..fdef1fd5f --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/AuthQualifier.kt @@ -0,0 +1,15 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthRepositoryQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthApiServiceQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentDetailQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentDetailQualifier.kt new file mode 100644 index 000000000..9e995d308 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentDetailQualifier.kt @@ -0,0 +1,15 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommentDetailRepositoryQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommentDetailDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommentDetailApiServiceQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentRoomsQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentRoomsQualifier.kt new file mode 100644 index 000000000..50e7c7a5e --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/CommentRoomsQualifier.kt @@ -0,0 +1,15 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommentRoomsRepositoryQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommentRoomsDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommentRoomsApiServiceQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/DataStoreQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/DataStoreQualifier.kt new file mode 100644 index 000000000..1cb2af513 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/DataStoreQualifier.kt @@ -0,0 +1,7 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class DataStoreQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingDetailQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingDetailQualifier.kt new file mode 100644 index 000000000..ec3dba7e3 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingDetailQualifier.kt @@ -0,0 +1,15 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingDetailRepositoryQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingDetailDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingDetailApiServiceQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingQualifier.kt new file mode 100644 index 000000000..f34b23d55 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/OfferingQualifier.kt @@ -0,0 +1,23 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingRepositoryQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingRemoteDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingLocalDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingApiServiceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OfferingDaoQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/annotations/ParticipantQualifier.kt b/android/app/src/main/java/com/zzang/chongdae/di/annotations/ParticipantQualifier.kt new file mode 100644 index 000000000..4dd2dd494 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/annotations/ParticipantQualifier.kt @@ -0,0 +1,15 @@ +package com.zzang.chongdae.di.annotations + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ParticipantRepositoryQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ParticipantDataSourceQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ParticipantApiServiceQualifier diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/AuthDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/AuthDependencyModule.kt new file mode 100644 index 000000000..497ca1f17 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/AuthDependencyModule.kt @@ -0,0 +1,40 @@ +package com.zzang.chongdae.di.module + +import com.zzang.chongdae.auth.api.AuthApiService +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.auth.repository.AuthRepositoryImpl +import com.zzang.chongdae.auth.source.AuthRemoteDataSource +import com.zzang.chongdae.auth.source.AuthRemoteDataSourceImpl +import com.zzang.chongdae.data.remote.api.NetworkManager +import com.zzang.chongdae.di.annotations.AuthApiServiceQualifier +import com.zzang.chongdae.di.annotations.AuthDataSourceQualifier +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class AuthDependencyModule { + @Binds + @Singleton + @AuthRepositoryQualifier + abstract fun provideAuthRepository(impl: AuthRepositoryImpl): AuthRepository + + @Binds + @Singleton + @AuthDataSourceQualifier + abstract fun provideAuthDataSource(impl: AuthRemoteDataSourceImpl): AuthRemoteDataSource + + companion object { + @Provides + @Singleton + @AuthApiServiceQualifier + fun provideAuthApiService(): AuthApiService { + return NetworkManager.authService() + } + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/CommentDetailDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/CommentDetailDependencyModule.kt new file mode 100644 index 000000000..785a57b8c --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/CommentDetailDependencyModule.kt @@ -0,0 +1,40 @@ +package com.zzang.chongdae.di.module + +import com.zzang.chongdae.data.remote.api.CommentApiService +import com.zzang.chongdae.data.remote.api.NetworkManager +import com.zzang.chongdae.data.remote.source.CommentRemoteDataSourceImpl +import com.zzang.chongdae.data.repository.CommentDetailRepositoryImpl +import com.zzang.chongdae.data.source.comment.CommentRemoteDataSource +import com.zzang.chongdae.di.annotations.CommentDetailApiServiceQualifier +import com.zzang.chongdae.di.annotations.CommentDetailDataSourceQualifier +import com.zzang.chongdae.di.annotations.CommentDetailRepositoryQualifier +import com.zzang.chongdae.domain.repository.CommentDetailRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class CommentDetailDependencyModule { + @Binds + @Singleton + @CommentDetailRepositoryQualifier + abstract fun provideCommentDetailRepository(impl: CommentDetailRepositoryImpl): CommentDetailRepository + + @Binds + @Singleton + @CommentDetailDataSourceQualifier + abstract fun provideCommentDetailDataSource(impl: CommentRemoteDataSourceImpl): CommentRemoteDataSource + + companion object { + @Provides + @Singleton + @CommentDetailApiServiceQualifier + fun provideCommentDetailApiService(): CommentApiService { + return NetworkManager.commentService() + } + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/CommentRoomsDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/CommentRoomsDependencyModule.kt new file mode 100644 index 000000000..56133633d --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/CommentRoomsDependencyModule.kt @@ -0,0 +1,40 @@ +package com.zzang.chongdae.di.module + +import com.zzang.chongdae.data.remote.api.CommentApiService +import com.zzang.chongdae.data.remote.api.NetworkManager +import com.zzang.chongdae.data.remote.source.CommentRoomsDataSourceImpl +import com.zzang.chongdae.data.repository.CommentRoomsRepositoryImpl +import com.zzang.chongdae.data.source.CommentRoomsDataSource +import com.zzang.chongdae.di.annotations.CommentRoomsApiServiceQualifier +import com.zzang.chongdae.di.annotations.CommentRoomsDataSourceQualifier +import com.zzang.chongdae.di.annotations.CommentRoomsRepositoryQualifier +import com.zzang.chongdae.domain.repository.CommentRoomsRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class CommentRoomsDependencyModule { + @Binds + @Singleton + @CommentRoomsRepositoryQualifier + abstract fun provideCommentRoomsRepository(impl: CommentRoomsRepositoryImpl): CommentRoomsRepository + + @Binds + @Singleton + @CommentRoomsDataSourceQualifier + abstract fun provideCommentRoomsDataSource(impl: CommentRoomsDataSourceImpl): CommentRoomsDataSource + + companion object { + @Provides + @Singleton + @CommentRoomsApiServiceQualifier + fun provideCommentRoomsService(): CommentApiService { + return NetworkManager.commentService() + } + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/DataStoreDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/DataStoreDependencyModule.kt new file mode 100644 index 000000000..eb05a65f0 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/DataStoreDependencyModule.kt @@ -0,0 +1,23 @@ +package com.zzang.chongdae.di.module + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import com.zzang.chongdae.ChongdaeApp +import com.zzang.chongdae.ChongdaeApp.Companion.dataStore +import com.zzang.chongdae.di.annotations.DataStoreQualifier +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object DataStoreDependencyModule { + @Provides + @Singleton + @DataStoreQualifier + fun provideDataStore(): DataStore { + return ChongdaeApp.chongdaeAppContext.dataStore + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDependencyModule.kt new file mode 100644 index 000000000..0a68002a1 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDependencyModule.kt @@ -0,0 +1,58 @@ +package com.zzang.chongdae.di.module + +import com.zzang.chongdae.ChongdaeApp +import com.zzang.chongdae.data.local.dao.OfferingDao +import com.zzang.chongdae.data.local.source.OfferingLocalDataSourceImpl +import com.zzang.chongdae.data.remote.api.NetworkManager +import com.zzang.chongdae.data.remote.api.OfferingApiService +import com.zzang.chongdae.data.remote.source.OfferingRemoteDataSourceImpl +import com.zzang.chongdae.data.repository.OfferingRepositoryImpl +import com.zzang.chongdae.data.source.offering.OfferingLocalDataSource +import com.zzang.chongdae.data.source.offering.OfferingRemoteDataSource +import com.zzang.chongdae.di.annotations.OfferingApiServiceQualifier +import com.zzang.chongdae.di.annotations.OfferingDaoQualifier +import com.zzang.chongdae.di.annotations.OfferingLocalDataSourceQualifier +import com.zzang.chongdae.di.annotations.OfferingRemoteDataSourceQualifier +import com.zzang.chongdae.di.annotations.OfferingRepositoryQualifier +import com.zzang.chongdae.domain.repository.OfferingRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class OfferingDependencyModule { + @Binds + @Singleton + @OfferingRepositoryQualifier + abstract fun provideOfferingRepository(impl: OfferingRepositoryImpl): OfferingRepository + + @Binds + @Singleton + @OfferingRemoteDataSourceQualifier + abstract fun provideOfferingRemoteDataSource(impl: OfferingRemoteDataSourceImpl): OfferingRemoteDataSource + + @Binds + @Singleton + @OfferingLocalDataSourceQualifier + abstract fun provideOfferingLocalDataSource(impl: OfferingLocalDataSourceImpl): OfferingLocalDataSource + + companion object { + @Provides + @Singleton + @OfferingApiServiceQualifier + fun provideOfferingService(): OfferingApiService { + return NetworkManager.offeringService() + } + + @Provides + @Singleton + @OfferingDaoQualifier + fun provideOfferingDao(): OfferingDao { + return ChongdaeApp.offeringDao + } + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDetailDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDetailDependencyModule.kt new file mode 100644 index 000000000..bb07a1747 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/OfferingDetailDependencyModule.kt @@ -0,0 +1,40 @@ +package com.zzang.chongdae.di.module + +import com.zzang.chongdae.data.remote.api.NetworkManager +import com.zzang.chongdae.data.remote.api.OfferingApiService +import com.zzang.chongdae.data.remote.source.OfferingDetailDataSourceImpl +import com.zzang.chongdae.data.repository.OfferingDetailRepositoryImpl +import com.zzang.chongdae.data.source.OfferingDetailDataSource +import com.zzang.chongdae.di.annotations.OfferingDetailApiServiceQualifier +import com.zzang.chongdae.di.annotations.OfferingDetailDataSourceQualifier +import com.zzang.chongdae.di.annotations.OfferingDetailRepositoryQualifier +import com.zzang.chongdae.domain.repository.OfferingDetailRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class OfferingDetailDependencyModule { + @Binds + @Singleton + @OfferingDetailRepositoryQualifier + abstract fun provideOfferingDetailRepository(impl: OfferingDetailRepositoryImpl): OfferingDetailRepository + + @Binds + @Singleton + @OfferingDetailDataSourceQualifier + abstract fun provideOfferingDetailDataSource(impl: OfferingDetailDataSourceImpl): OfferingDetailDataSource + + companion object { + @Provides + @Singleton + @OfferingDetailApiServiceQualifier + fun provideOfferingDetailService(): OfferingApiService { + return NetworkManager.offeringService() + } + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/di/module/ParticipantDependencyModule.kt b/android/app/src/main/java/com/zzang/chongdae/di/module/ParticipantDependencyModule.kt new file mode 100644 index 000000000..0bb581b0d --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/di/module/ParticipantDependencyModule.kt @@ -0,0 +1,40 @@ +package com.zzang.chongdae.di.module + +import com.zzang.chongdae.data.remote.api.NetworkManager +import com.zzang.chongdae.data.remote.api.ParticipationApiService +import com.zzang.chongdae.data.remote.source.ParticipantRemoteDataSourceImpl +import com.zzang.chongdae.data.repository.ParticipantRepositoryImpl +import com.zzang.chongdae.data.source.ParticipantRemoteDataSource +import com.zzang.chongdae.di.annotations.ParticipantApiServiceQualifier +import com.zzang.chongdae.di.annotations.ParticipantDataSourceQualifier +import com.zzang.chongdae.di.annotations.ParticipantRepositoryQualifier +import com.zzang.chongdae.domain.repository.ParticipantRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class ParticipantDependencyModule { + @Binds + @Singleton + @ParticipantRepositoryQualifier + abstract fun provideParticipantRepository(impl: ParticipantRepositoryImpl): ParticipantRepository + + @Binds + @Singleton + @ParticipantDataSourceQualifier + abstract fun provideParticipantDataSource(impl: ParticipantRemoteDataSourceImpl): ParticipantRemoteDataSource + + companion object { + @Provides + @Singleton + @ParticipantApiServiceQualifier + fun provideParticipantApiService(): ParticipationApiService { + return NetworkManager.participationService() + } + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingDetail.kt b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingDetail.kt index 262d50601..04b1c5779 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingDetail.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingDetail.kt @@ -11,6 +11,7 @@ data class OfferingDetail( val thumbnailUrl: String?, val dividedPrice: Int, val totalPrice: Int, + val originPrice: Int?, val meetingDate: LocalDateTime, val currentCount: CurrentCount, val totalCount: Int, diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteUiModel.kt b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainRequest.kt similarity index 79% rename from android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteUiModel.kt rename to android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainRequest.kt index 808096314..1767efc73 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteUiModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainRequest.kt @@ -1,6 +1,6 @@ -package com.zzang.chongdae.presentation.view.write +package com.zzang.chongdae.domain.model -data class OfferingWriteUiModel( +data class OfferingModifyDomainRequest( val title: String, val productUrl: String?, val thumbnailUrl: String?, diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainResponse.kt b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainResponse.kt new file mode 100644 index 000000000..20b5e5447 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingModifyDomainResponse.kt @@ -0,0 +1,20 @@ +package com.zzang.chongdae.domain.model + +import java.time.LocalDateTime + +data class OfferingModifyDomainResponse( + val id: Long, + val title: String, + val productUrl: String?, + val meetingAddress: String, + val meetingAddressDetail: String, + val description: String, + val meetingDate: LocalDateTime, + val currentCount: CurrentCount, + val totalCount: Int, + val thumbnailUrl: String?, + val dividedPrice: Int, + val totalPrice: Int, + val condition: OfferingCondition, + val nickname: String, +) diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingWrite.kt b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingWrite.kt new file mode 100644 index 000000000..3bab29f02 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/domain/model/OfferingWrite.kt @@ -0,0 +1,15 @@ +package com.zzang.chongdae.domain.model + +data class OfferingWrite( + val title: String, + val productUrl: String?, + val thumbnailUrl: String?, + val totalCount: Int, + val totalPrice: Int, + val originPrice: Int?, + val meetingAddress: String, + val meetingAddressDong: String?, + val meetingAddressDetail: String, + val meetingDate: String, + val description: String, +) diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/paging/OfferingPagingSource.kt b/android/app/src/main/java/com/zzang/chongdae/domain/paging/OfferingPagingSource.kt index b07e7afd0..be354f405 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/paging/OfferingPagingSource.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/paging/OfferingPagingSource.kt @@ -2,11 +2,11 @@ package com.zzang.chongdae.domain.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.domain.model.Offering -import com.zzang.chongdae.domain.repository.AuthRepository import com.zzang.chongdae.domain.repository.OfferingRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result class OfferingPagingSource( private val offeringsRepository: OfferingRepository, diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/repository/AuthRepository.kt b/android/app/src/main/java/com/zzang/chongdae/domain/repository/AuthRepository.kt deleted file mode 100644 index 30b6d0db6..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/domain/repository/AuthRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.zzang.chongdae.domain.repository - -import com.zzang.chongdae.domain.model.Member -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result - -interface AuthRepository { - suspend fun saveLogin(accessToken: String): Result - - suspend fun saveRefresh(): Result -} diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentDetailRepository.kt b/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentDetailRepository.kt index 0b0daa896..4dc188623 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentDetailRepository.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentDetailRepository.kt @@ -1,9 +1,9 @@ package com.zzang.chongdae.domain.repository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.domain.model.Comment import com.zzang.chongdae.domain.model.CommentOfferingInfo -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface CommentDetailRepository { suspend fun saveComment( diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentRoomsRepository.kt b/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentRoomsRepository.kt index e5d883694..091c7ae87 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentRoomsRepository.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/repository/CommentRoomsRepository.kt @@ -1,8 +1,8 @@ package com.zzang.chongdae.domain.repository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.domain.model.CommentRoom -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface CommentRoomsRepository { suspend fun fetchCommentRooms(): Result, DataError.Network> diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingDetailRepository.kt b/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingDetailRepository.kt index 28e682f16..d9bb30449 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingDetailRepository.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingDetailRepository.kt @@ -1,11 +1,13 @@ package com.zzang.chongdae.domain.repository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.domain.model.OfferingDetail -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface OfferingDetailRepository { suspend fun fetchOfferingDetail(offeringId: Long): Result suspend fun saveParticipation(offeringId: Long): Result + + suspend fun deleteOffering(offeringId: Long): Result } diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingRepository.kt b/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingRepository.kt index b0ba9e717..f828fde38 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingRepository.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/repository/OfferingRepository.kt @@ -1,12 +1,13 @@ package com.zzang.chongdae.domain.repository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.domain.model.Filter import com.zzang.chongdae.domain.model.Meetings import com.zzang.chongdae.domain.model.Offering +import com.zzang.chongdae.domain.model.OfferingModifyDomainRequest +import com.zzang.chongdae.domain.model.OfferingWrite import com.zzang.chongdae.domain.model.ProductUrl -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result -import com.zzang.chongdae.presentation.view.write.OfferingWriteUiModel import okhttp3.MultipartBody interface OfferingRepository { @@ -19,7 +20,7 @@ interface OfferingRepository { pageSize: Int?, ): Result, DataError.Network> - suspend fun saveOffering(uiModel: OfferingWriteUiModel): Result + suspend fun saveOffering(offeringWrite: OfferingWrite): Result suspend fun saveProductImageOg(productUrl: String): Result @@ -28,4 +29,9 @@ interface OfferingRepository { suspend fun fetchFilters(): Result, DataError.Network> suspend fun fetchMeetings(offeringId: Long): Result + + suspend fun patchOffering( + offeringId: Long, + offeringModifyDomainRequest: OfferingModifyDomainRequest, + ): Result } diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/repository/ParticipantRepository.kt b/android/app/src/main/java/com/zzang/chongdae/domain/repository/ParticipantRepository.kt index 07e502fd0..88c086054 100644 --- a/android/app/src/main/java/com/zzang/chongdae/domain/repository/ParticipantRepository.kt +++ b/android/app/src/main/java/com/zzang/chongdae/domain/repository/ParticipantRepository.kt @@ -1,8 +1,8 @@ package com.zzang.chongdae.domain.repository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result import com.zzang.chongdae.domain.model.participant.Participants -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result interface ParticipantRepository { suspend fun fetchParticipants(offeringId: Long): Result diff --git a/android/app/src/main/java/com/zzang/chongdae/domain/util/Error.kt b/android/app/src/main/java/com/zzang/chongdae/domain/util/Error.kt deleted file mode 100644 index cb8861cc6..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/domain/util/Error.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.zzang.chongdae.domain.util - -sealed interface Error diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/util/AccessTokenExpirationHandler.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/util/AccessTokenExpirationHandler.kt deleted file mode 100644 index 1f2bcbccd..000000000 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/util/AccessTokenExpirationHandler.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.zzang.chongdae.presentation.util - -import android.util.Log -import com.zzang.chongdae.domain.model.HttpStatusCode -import com.zzang.chongdae.domain.repository.AuthRepository - -suspend fun handleAccessTokenExpiration( - authRepository: AuthRepository, - it: Throwable, - retryFunction: () -> Unit, -) { - when (it.message) { - HttpStatusCode.UNAUTHORIZED_401.code -> { - Log.e("error", "Access Token 만료") - authRepository.saveRefresh() - retryFunction() - } - } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/util/BindingAdapters.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/util/BindingAdapters.kt index 35fcd013a..4bf4eec18 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/util/BindingAdapters.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/util/BindingAdapters.kt @@ -336,3 +336,14 @@ private fun TextView.setDiscountRate( fun EditText.setOriginPriceHint(originPrice: Int) { this.hint = context.getString(R.string.write_current_split_price).format(originPrice) } + +@BindingAdapter(value = ["debouncedOnClick", "debounceTime"], requireAll = false) +fun View.setDebouncedOnClick( + clickListener: View.OnClickListener?, + debounceTime: Long?, +) { + val safeDebounceTime = debounceTime ?: DEFAULT_DEBOUNCE_TIME + setDebouncedOnClickListener(safeDebounceTime) { + clickListener?.onClick(this) + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/util/DebouncedClickListener.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/util/DebouncedClickListener.kt new file mode 100644 index 000000000..fde368c31 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/util/DebouncedClickListener.kt @@ -0,0 +1,21 @@ +package com.zzang.chongdae.presentation.util + +import android.os.SystemClock +import android.view.View + +fun View.setDebouncedOnClickListener( + debounceTime: Long = DEFAULT_DEBOUNCE_TIME, + action: (View) -> Unit, +) { + var lastClickTime = 0L + + this.setOnClickListener { + val currentTime = SystemClock.elapsedRealtime() + if (currentTime - lastClickTime >= debounceTime) { + lastClickTime = currentTime + action(it) + } + } +} + +const val DEFAULT_DEBOUNCE_TIME = 200L diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/MainActivity.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/MainActivity.kt index 9be6d959d..04eba11eb 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/MainActivity.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/MainActivity.kt @@ -2,18 +2,24 @@ package com.zzang.chongdae.presentation.view import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.MotionEvent import android.view.View import android.view.inputmethod.InputMethodManager +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController import com.zzang.chongdae.R import com.zzang.chongdae.databinding.ActivityMainBinding +import com.zzang.chongdae.presentation.view.offeringdetail.OfferingDetailFragment +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : AppCompatActivity() { private var _binding: ActivityMainBinding? = null private val binding get() = _binding!! @@ -25,6 +31,7 @@ class MainActivity : AppCompatActivity() { initBinding() initNavController() setupBottomNavigation() + handleDeepLink(intent) } private fun initBinding() { @@ -57,12 +64,45 @@ class MainActivity : AppCompatActivity() { return super.dispatchTouchEvent(motionEvent) } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleDeepLink(intent) + } + + private fun handleDeepLink(intent: Intent) { + val data: Uri? = intent.data + data?.let { uri -> + if (uri.scheme == SCHEME && uri.host == HOST) { + val offeringIdStr = uri.lastPathSegment + + val offeringId = offeringIdStr?.toLongOrNull() + if (offeringId != null) { + openOfferingDetailFragment(offeringId) + } else { + Toast.makeText(this, "공모 ID가 올바르지 않습니다.", Toast.LENGTH_SHORT).show() + } + } else { + Toast.makeText(this, "Deeplink가 올바르지 않습니다.", Toast.LENGTH_SHORT).show() + } + } + } + + private fun openOfferingDetailFragment(offeringId: Long) { + val navController = navHostFragment.navController + val bundle = bundleOf(OfferingDetailFragment.OFFERING_ID_KEY to offeringId) + + navController.navigate(R.id.action_home_fragment_to_offering_detail_fragment, bundle) + } + override fun onDestroy() { super.onDestroy() _binding = null } companion object { + private const val SCHEME = "chongdaeapp" + private const val HOST = "offerings" + fun startActivity(context: Context) = Intent(context, MainActivity::class.java).run { context.startActivity(this) diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsFragment.kt index b804f5a6c..8d5b35bf2 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsFragment.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsFragment.kt @@ -8,13 +8,14 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager import com.zzang.chongdae.databinding.FragmentCommentRoomsBinding -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager import com.zzang.chongdae.presentation.view.comment.adapter.CommentRoomsAdapter import com.zzang.chongdae.presentation.view.comment.adapter.OnCommentRoomClickListener import com.zzang.chongdae.presentation.view.commentdetail.CommentDetailActivity +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class CommentRoomsFragment : Fragment(), OnCommentRoomClickListener { private var _binding: FragmentCommentRoomsBinding? = null private val binding get() = _binding!! @@ -30,12 +31,7 @@ class CommentRoomsFragment : Fragment(), OnCommentRoomClickListener { CommentRoomsAdapter(this) } - private val viewModel by viewModels { - CommentRoomsViewModel.getFactory( - authRepository = (requireActivity().application as ChongdaeApp).authRepository, - commentRoomsRepository = (requireActivity().application as ChongdaeApp).commentRoomsRepository, - ) - } + private val viewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsViewModel.kt index ebaf03186..e67720024 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/comment/CommentRoomsViewModel.kt @@ -4,56 +4,52 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map import androidx.lifecycle.viewModelScope +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import com.zzang.chongdae.di.annotations.CommentRoomsRepositoryQualifier import com.zzang.chongdae.domain.model.CommentRoom -import com.zzang.chongdae.domain.repository.AuthRepository import com.zzang.chongdae.domain.repository.CommentRoomsRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class CommentRoomsViewModel( - private val authRepository: AuthRepository, - private val commentRoomsRepository: CommentRoomsRepository, -) : ViewModel() { - private val _commentRooms: MutableLiveData> = MutableLiveData() - val commentRooms: LiveData> get() = _commentRooms +@HiltViewModel +class CommentRoomsViewModel + @Inject + constructor( + @AuthRepositoryQualifier private val authRepository: AuthRepository, + @CommentRoomsRepositoryQualifier private val commentRoomsRepository: CommentRoomsRepository, + ) : ViewModel() { + private val _commentRooms: MutableLiveData> = MutableLiveData() + val commentRooms: LiveData> get() = _commentRooms - val isCommentRoomsEmpty: LiveData - get() = - commentRooms.map { - it.isEmpty() - } + val isCommentRoomsEmpty: LiveData + get() = + commentRooms.map { + it.isEmpty() + } - fun updateCommentRooms() { - viewModelScope.launch { - when (val result = commentRoomsRepository.fetchCommentRooms()) { - is Result.Error -> { - Log.e("error", "${result.error}") - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - updateCommentRooms() + fun updateCommentRooms() { + viewModelScope.launch { + when (val result = commentRoomsRepository.fetchCommentRooms()) { + is Result.Error -> { + Log.e("error", "updateCommentRooms: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> updateCommentRooms() + is Result.Error -> return@launch + } + } + else -> {} } - else -> {} } + is Result.Success -> _commentRooms.value = result.data } - is Result.Success -> _commentRooms.value = result.data - } - } - } - - companion object { - @Suppress("UNCHECKED_CAST") - fun getFactory( - authRepository: AuthRepository, - commentRoomsRepository: CommentRoomsRepository, - ) = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return CommentRoomsViewModel(authRepository, commentRoomsRepository) as T } } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt index 33d45c565..f3ab4ed57 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Bundle import android.view.MotionEvent import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -15,14 +16,18 @@ import androidx.core.view.doOnPreDraw import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.LinearLayoutManager import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager import com.zzang.chongdae.databinding.ActivityCommentDetailBinding +import com.zzang.chongdae.databinding.DialogAlertBinding import com.zzang.chongdae.databinding.DialogUpdateStatusBinding -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager +import com.zzang.chongdae.presentation.util.setDebouncedOnClickListener import com.zzang.chongdae.presentation.view.commentdetail.adapter.comment.CommentAdapter import com.zzang.chongdae.presentation.view.commentdetail.adapter.participant.ParticipantAdapter +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +@AndroidEntryPoint class CommentDetailActivity : AppCompatActivity(), OnUpdateStatusClickListener { private var _binding: ActivityCommentDetailBinding? = null private val binding get() = _binding!! @@ -31,13 +36,13 @@ class CommentDetailActivity : AppCompatActivity(), OnUpdateStatusClickListener { private val participantAdapter: ParticipantAdapter by lazy { ParticipantAdapter() } private val dialog: Dialog by lazy { Dialog(this) } + @Inject + lateinit var commentDetailAssistedFactory: CommentDetailViewModel.CommentDetailAssistedFactory + private val viewModel: CommentDetailViewModel by viewModels { CommentDetailViewModel.getFactory( + assistedFactory = commentDetailAssistedFactory, offeringId = offeringId, - authRepository = (application as ChongdaeApp).authRepository, - offeringRepository = (application as ChongdaeApp).offeringRepository, - participantRepository = (application as ChongdaeApp).participantRepository, - commentDetailRepository = (application as ChongdaeApp).commentDetailRepository, ) } @@ -70,10 +75,10 @@ class CommentDetailActivity : AppCompatActivity(), OnUpdateStatusClickListener { } private fun setupDrawerToggle() { - binding.ivMoreOptions.setOnClickListener { + binding.ivMoreOptions.setDebouncedOnClickListener { if (binding.drawerLayout.isDrawerOpen(GravityCompat.END)) { binding.drawerLayout.closeDrawer(GravityCompat.END) - return@setOnClickListener + return@setDebouncedOnClickListener } binding.drawerLayout.openDrawer(GravityCompat.END) firebaseAnalyticsManager.logSelectContentEvent( @@ -151,6 +156,18 @@ class CommentDetailActivity : AppCompatActivity(), OnUpdateStatusClickListener { contentType = "button", ) finish() + dialog.dismiss() + } + viewModel.showAlertEvent.observe(this) { + val alertBinding = DialogAlertBinding.inflate(layoutInflater, null, false) + alertBinding.tvDialogMessage.text = getString(R.string.comment_detail_exit_alert) + alertBinding.listener = viewModel + + dialog.setContentView(alertBinding.root) + dialog.show() + } + viewModel.alertCancelEvent.observe(this) { + dialog.dismiss() } } @@ -214,8 +231,19 @@ class CommentDetailActivity : AppCompatActivity(), OnUpdateStatusClickListener { } override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean { - (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).apply { - this.hideSoftInputFromWindow(currentFocus?.windowToken, 0) + val view = currentFocus + if (view != null && (view is EditText || view.id == R.id.iv_send_comment)) { + val screenCoords = IntArray(2) + view.getLocationOnScreen(screenCoords) + val x = motionEvent.rawX + view.left - screenCoords[0] + val y = motionEvent.rawY + view.top - screenCoords[1] + + if (motionEvent.action == MotionEvent.ACTION_UP && + (x < view.left || x >= view.right || y < view.top || y > view.bottom) + ) { + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } } return super.dispatchTouchEvent(motionEvent) } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt index 7902f70be..d67785fb6 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt @@ -5,15 +5,18 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import com.zzang.chongdae.R +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import com.zzang.chongdae.di.annotations.CommentDetailRepositoryQualifier +import com.zzang.chongdae.di.annotations.OfferingRepositoryQualifier +import com.zzang.chongdae.di.annotations.ParticipantRepositoryQualifier import com.zzang.chongdae.domain.model.Comment -import com.zzang.chongdae.domain.repository.AuthRepository import com.zzang.chongdae.domain.repository.CommentDetailRepository import com.zzang.chongdae.domain.repository.OfferingRepository import com.zzang.chongdae.domain.repository.ParticipantRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result import com.zzang.chongdae.presentation.util.MutableSingleLiveData import com.zzang.chongdae.presentation.util.SingleLiveData import com.zzang.chongdae.presentation.view.commentdetail.model.information.CommentOfferingInfoUiModel @@ -22,269 +25,308 @@ import com.zzang.chongdae.presentation.view.commentdetail.model.meeting.Meetings import com.zzang.chongdae.presentation.view.commentdetail.model.meeting.MeetingsUiModel.Companion.toUiModel import com.zzang.chongdae.presentation.view.commentdetail.model.participants.ParticipantsUiModel import com.zzang.chongdae.presentation.view.commentdetail.model.participants.ParticipantsUiModel.Companion.toUiModel +import com.zzang.chongdae.presentation.view.common.OnAlertClickListener +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -class CommentDetailViewModel( - private val offeringId: Long, - private val authRepository: AuthRepository, - private val offeringRepository: OfferingRepository, - private val participantRepository: ParticipantRepository, - private val commentDetailRepository: CommentDetailRepository, -) : ViewModel() { - private var cachedComments: List = emptyList() - private var pollJob: Job? = null - val commentContent = MutableLiveData("") +class CommentDetailViewModel + @AssistedInject + constructor( + @Assisted private val offeringId: Long, + @AuthRepositoryQualifier private val authRepository: AuthRepository, + @OfferingRepositoryQualifier private val offeringRepository: OfferingRepository, + @ParticipantRepositoryQualifier private val participantRepository: ParticipantRepository, + @CommentDetailRepositoryQualifier private val commentDetailRepository: CommentDetailRepository, + ) : ViewModel(), + OnAlertClickListener { + @AssistedFactory + interface CommentDetailAssistedFactory { + fun create(offeringId: Long): CommentDetailViewModel + } - private val _comments: MutableLiveData> = MutableLiveData() - val comments: LiveData> get() = _comments + private var cachedComments: List = emptyList() + private var pollJob: Job? = null + val commentContent = MutableLiveData("") - private val _commentOfferingInfo = MutableLiveData() - val commentOfferingInfo: LiveData get() = _commentOfferingInfo + private val _comments: MutableLiveData> = MutableLiveData() + val comments: LiveData> get() = _comments - private val _meetings = MutableLiveData() - val meetings: LiveData get() = _meetings + private val _commentOfferingInfo = MutableLiveData() + val commentOfferingInfo: LiveData get() = _commentOfferingInfo - private val _isCollapsibleViewVisible = MutableLiveData(false) - val isCollapsibleViewVisible: LiveData get() = _isCollapsibleViewVisible + private val _meetings = MutableLiveData() + val meetings: LiveData get() = _meetings - private val _participants = MutableLiveData() - val participants: LiveData get() = _participants + private val _isCollapsibleViewVisible = MutableLiveData(false) + val isCollapsibleViewVisible: LiveData get() = _isCollapsibleViewVisible - private val _showStatusDialogEvent = MutableLiveData() - val showStatusDialogEvent: LiveData get() = _showStatusDialogEvent + private val _participants = MutableLiveData() + val participants: LiveData get() = _participants - private val _reportEvent: MutableSingleLiveData = MutableSingleLiveData() - val reportEvent: SingleLiveData get() = _reportEvent + private val _showStatusDialogEvent = MutableLiveData() + val showStatusDialogEvent: LiveData get() = _showStatusDialogEvent - private val _onExitOfferingEvent = MutableSingleLiveData() - val onExitOfferingEvent: SingleLiveData get() = _onExitOfferingEvent + private val _reportEvent: MutableSingleLiveData = MutableSingleLiveData() + val reportEvent: SingleLiveData get() = _reportEvent - private val _onBackPressedEvent = MutableSingleLiveData() - val onBackPressedEvent: SingleLiveData get() = _onBackPressedEvent + private val _onExitOfferingEvent = MutableSingleLiveData() + val onExitOfferingEvent: SingleLiveData get() = _onExitOfferingEvent - private val _errorEvent = MutableLiveData() - val errorEvent: MutableLiveData get() = _errorEvent + private val _onBackPressedEvent = MutableSingleLiveData() + val onBackPressedEvent: SingleLiveData get() = _onBackPressedEvent - init { - startPolling() - updateCommentInfo() - loadMeetings() - loadParticipants() - } + private val _errorEvent = MutableLiveData() + val errorEvent: MutableLiveData get() = _errorEvent - private fun startPolling() { - pollJob?.cancel() - pollJob = - viewModelScope.launch { - while (this.isActive) { - loadComments() - delay(1000) + private val _showAlertEvent = MutableSingleLiveData() + val showAlertEvent: SingleLiveData get() = _showAlertEvent + + private val _alertCancelEvent = MutableSingleLiveData() + val alertCancelEvent: SingleLiveData get() = _alertCancelEvent + + init { + startPolling() + updateCommentInfo() + loadMeetings() + loadParticipants() + } + + private fun startPolling() { + pollJob?.cancel() + pollJob = + viewModelScope.launch { + while (this.isActive) { + loadComments() + delay(1000) + } } - } - } + } - private fun updateCommentInfo() { - viewModelScope.launch { - when (val result = commentDetailRepository.fetchCommentOfferingInfo(offeringId)) { - is Result.Success -> _commentOfferingInfo.value = result.data.toUiModel() - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - updateCommentInfo() - } - else -> { - errorEvent.value = result.error.name + private fun updateCommentInfo() { + viewModelScope.launch { + when (val result = commentDetailRepository.fetchCommentOfferingInfo(offeringId)) { + is Result.Success -> _commentOfferingInfo.value = result.data.toUiModel() + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> updateCommentInfo() + is Result.Error -> return@launch + } + } + + else -> { + errorEvent.value = result.error.name + } } - } + } } } - } - fun updateOfferingEvent() { - _showStatusDialogEvent.value = Unit - } + fun updateOfferingEvent() { + _showStatusDialogEvent.value = Unit + } - fun updateOfferingStatus() { - viewModelScope.launch { - when (val result = commentDetailRepository.updateOfferingStatus(offeringId)) { - is Result.Success -> updateCommentInfo() - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - updateOfferingStatus() + fun updateOfferingStatus() { + viewModelScope.launch { + when (val result = commentDetailRepository.updateOfferingStatus(offeringId)) { + is Result.Success -> updateCommentInfo() + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> updateOfferingStatus() + is Result.Error -> return@launch + } + } + + else -> { + errorEvent.value = result.error.name + } } + } + } + } - else -> { - errorEvent.value = result.error.name + fun loadComments() { + viewModelScope.launch { + when (val result = commentDetailRepository.fetchComments(offeringId)) { + is Result.Success -> { + val newComments = result.data + if (cachedComments != newComments) { + _comments.value = newComments + cachedComments = newComments } } + + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> loadComments() + is Result.Error -> return@launch + } + } + + else -> { + pollJob?.cancel() + errorEvent.value = result.error.name + } + } + } } } - } - fun loadComments() { - viewModelScope.launch { - when (val result = commentDetailRepository.fetchComments(offeringId)) { - is Result.Success -> { - val newComments = result.data - if (cachedComments != newComments) { - _comments.value = newComments - cachedComments = newComments + fun postComment() { + val content = commentContent.value?.trim() + if (content.isNullOrEmpty()) { + return + } + viewModelScope.launch { + when (val result = commentDetailRepository.saveComment(offeringId, content)) { + is Result.Success -> { + commentContent.value = "" } - } - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - loadComments() - } - else -> { - pollJob?.cancel() - errorEvent.value = result.error.name + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> postComment() + is Result.Error -> return@launch + } + } + + else -> { + errorEvent.value = result.error.name + } } - } + } } } - } - fun postComment() { - val content = commentContent.value?.trim() - if (content.isNullOrEmpty()) { - return + fun toggleCollapsibleView() { + _isCollapsibleViewVisible.value = _isCollapsibleViewVisible.value?.not() + if (_isCollapsibleViewVisible.value == true) { + loadMeetings() + } } - viewModelScope.launch { - when (val result = commentDetailRepository.saveComment(offeringId, content)) { - is Result.Success -> { - commentContent.value = "" + private fun loadParticipants() { + viewModelScope.launch { + when (val result = participantRepository.fetchParticipants(offeringId)) { + is Result.Success -> _participants.value = result.data.toUiModel() + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> loadParticipants() + is Result.Error -> return@launch + } + } + + else -> { + errorEvent.value = result.error.name + } + } } + } + } - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - postComment() - } - else -> { - errorEvent.value = result.error.name + private fun loadMeetings() { + viewModelScope.launch { + when (val result = offeringRepository.fetchMeetings(offeringId)) { + is Result.Success -> _meetings.value = result.data.toUiModel() + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> loadMeetings() + is Result.Error -> return@launch + } + } + + else -> { + errorEvent.value = result.error.name + } } - } + } } } - } - fun toggleCollapsibleView() { - _isCollapsibleViewVisible.value = _isCollapsibleViewVisible.value?.not() - if (_isCollapsibleViewVisible.value == true) { - loadMeetings() + fun onClickReport() { + _reportEvent.setValue(R.string.report_url) } - } - private fun loadParticipants() { - viewModelScope.launch { - when (val result = participantRepository.fetchParticipants(offeringId)) { - is Result.Success -> _participants.value = result.data.toUiModel() - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - loadParticipants() - } - else -> { - errorEvent.value = result.error.name - } + fun exitOffering() { + viewModelScope.launch { + when (val result = participantRepository.deleteParticipations(offeringId)) { + is Result.Success -> { + _onExitOfferingEvent.setValue(Unit) + pollJob?.cancel() } - } - } - } - private fun loadMeetings() { - viewModelScope.launch { - when (val result = offeringRepository.fetchMeetings(offeringId)) { - is Result.Success -> _meetings.value = result.data.toUiModel() - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - loadMeetings() + is Result.Error -> + when (result.error) { + DataError.Network.NULL -> { + _onExitOfferingEvent.setValue(Unit) + pollJob?.cancel() + } + + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> exitOffering() + is Result.Error -> return@launch + } + } + + else -> { + _errorEvent.value = result.error.name + } } - else -> { - errorEvent.value = result.error.name - } - } + } } } - } - fun onClickReport() { - _reportEvent.setValue(R.string.report_url) - } + fun onBackClick() { + _onBackPressedEvent.setValue(Unit) + } - fun exitOffering() { - viewModelScope.launch { - when (val result = participantRepository.deleteParticipations(offeringId)) { - is Result.Success -> { - _onExitOfferingEvent.setValue(Unit) - pollJob?.cancel() + override fun onCleared() { + super.onCleared() + stopPolling() + } + + private fun stopPolling() { + pollJob?.cancel() + } + + companion object { + @Suppress("UNCHECKED_CAST") + fun getFactory( + assistedFactory: CommentDetailAssistedFactory, + offeringId: Long, + ) = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return assistedFactory.create(offeringId) as T } - is Result.Error -> - when (result.error) { - DataError.Network.NULL -> { - _onExitOfferingEvent.setValue(Unit) - pollJob?.cancel() - } - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - exitOffering() - } - else -> { - _errorEvent.value = result.error.name - } - } } } - } - fun onBackClick() { - _onBackPressedEvent.setValue(Unit) - } - - override fun onCleared() { - super.onCleared() - stopPolling() - } + fun onExitClick() { + _showAlertEvent.setValue(Unit) + } - private fun stopPolling() { - pollJob?.cancel() - } + override fun onClickConfirm() { + exitOffering() + } - companion object { - @Suppress("UNCHECKED_CAST") - fun getFactory( - offeringId: Long, - authRepository: AuthRepository, - offeringRepository: OfferingRepository, - participantRepository: ParticipantRepository, - commentDetailRepository: CommentDetailRepository, - ) = object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - return CommentDetailViewModel( - offeringId = offeringId, - authRepository = authRepository, - offeringRepository = offeringRepository, - participantRepository = participantRepository, - commentDetailRepository = commentDetailRepository, - ) as T - } + override fun onClickCancel() { + _alertCancelEvent.setValue(Unit) } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/common/OnAlertClickListener.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/common/OnAlertClickListener.kt new file mode 100644 index 000000000..183af25ec --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/common/OnAlertClickListener.kt @@ -0,0 +1,7 @@ +package com.zzang.chongdae.presentation.view.common + +interface OnAlertClickListener { + fun onClickConfirm() + + fun onClickCancel() +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/HomeFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/HomeFragment.kt index 81fcd2dd5..ae52cdfd0 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/HomeFragment.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/HomeFragment.kt @@ -22,33 +22,27 @@ import androidx.paging.PagingData import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp -import com.zzang.chongdae.ChongdaeApp.Companion.dataStore import com.zzang.chongdae.R -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager import com.zzang.chongdae.databinding.FragmentHomeBinding import com.zzang.chongdae.domain.model.FilterName -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager +import com.zzang.chongdae.presentation.util.setDebouncedOnClickListener import com.zzang.chongdae.presentation.view.MainActivity import com.zzang.chongdae.presentation.view.home.adapter.OfferingAdapter import com.zzang.chongdae.presentation.view.login.LoginActivity import com.zzang.chongdae.presentation.view.offeringdetail.OfferingDetailFragment import com.zzang.chongdae.presentation.view.write.OfferingWriteOptionalFragment +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +@AndroidEntryPoint class HomeFragment : Fragment(), OnOfferingClickListener { private var _binding: FragmentHomeBinding? = null private val binding get() = _binding!! private var toast: Toast? = null private lateinit var offeringAdapter: OfferingAdapter - private val viewModel: OfferingViewModel by viewModels { - OfferingViewModel.getFactory( - offeringRepository = (requireActivity().application as ChongdaeApp).offeringRepository, - authRepository = (requireActivity().applicationContext as ChongdaeApp).authRepository, - userPreferencesDataStore = UserPreferencesDataStore(requireActivity().applicationContext.dataStore), - ) - } + private val viewModel: OfferingViewModel by viewModels() private val firebaseAnalytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(requireContext()) @@ -77,6 +71,14 @@ class HomeFragment : Fragment(), OnOfferingClickListener { navigateToOfferingWriteFragment() initFragmentResultListener() setOnCheckboxListener() + setOnSwipeRefreshListener() + } + + private fun setOnSwipeRefreshListener() { + binding.swipeLayout.setOnRefreshListener { + binding.swipeLayout.isRefreshing = false + viewModel.swipeRefresh() + } } private fun setOnCheckboxListener() { @@ -119,8 +121,12 @@ class HomeFragment : Fragment(), OnOfferingClickListener { viewModel.fetchUpdatedOffering(bundle.getLong(OfferingDetailFragment.UPDATED_OFFERING_ID_KEY)) } + setFragmentResultListener(OfferingDetailFragment.OFFERING_DETAIL_BUNDLE_KEY) { _, bundle -> + viewModel.refreshOfferings(bundle.getBoolean(OfferingDetailFragment.DELETED_OFFERING_ID_KEY)) + } + setFragmentResultListener(OfferingWriteOptionalFragment.OFFERING_WRITE_BUNDLE_KEY) { _, bundle -> - viewModel.refreshOfferingsByOfferingWriteEvent( + viewModel.refreshOfferings( bundle.getBoolean( OfferingWriteOptionalFragment.NEW_OFFERING_EVENT_KEY, ), @@ -164,6 +170,13 @@ class HomeFragment : Fragment(), OnOfferingClickListener { private fun initAdapter() { offeringAdapter = OfferingAdapter(this) + offeringAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + binding.tvEmptyItem.isVisible = isItemEmpty() + } else { + binding.tvEmptyItem.isVisible = false + } + } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { offeringAdapter.loadStateFlow.collect { loadState -> @@ -180,6 +193,8 @@ class HomeFragment : Fragment(), OnOfferingClickListener { ) } + private fun isItemEmpty() = offeringAdapter.itemCount == 0 + private fun setUpOfferingsObserve() { viewModel.offeringsRefreshEvent.observe(viewLifecycleOwner) { offeringAdapter.submitData(viewLifecycleOwner.lifecycle, PagingData.empty()) @@ -229,7 +244,7 @@ class HomeFragment : Fragment(), OnOfferingClickListener { } private fun navigateToOfferingWriteFragment() { - binding.fabCreateOffering.setOnClickListener { + binding.fabCreateOffering.setDebouncedOnClickListener { findNavController().navigate(R.id.action_home_fragment_to_offering_write_fragment) } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/OfferingViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/OfferingViewModel.kt index c593cd553..d8ec6d13d 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/OfferingViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/home/OfferingViewModel.kt @@ -4,239 +4,234 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map import com.zzang.chongdae.R -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.datastore.UserPreferencesDataStore +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import com.zzang.chongdae.di.annotations.OfferingRepositoryQualifier import com.zzang.chongdae.domain.model.Filter import com.zzang.chongdae.domain.model.FilterName import com.zzang.chongdae.domain.model.Offering import com.zzang.chongdae.domain.paging.OfferingPagingSource -import com.zzang.chongdae.domain.repository.AuthRepository import com.zzang.chongdae.domain.repository.OfferingRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result import com.zzang.chongdae.presentation.util.MutableSingleLiveData import com.zzang.chongdae.presentation.util.SingleLiveData +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OfferingViewModel + @Inject + constructor( + @OfferingRepositoryQualifier private val offeringRepository: OfferingRepository, + @AuthRepositoryQualifier private val authRepository: AuthRepository, + private val userPreferencesDataStore: UserPreferencesDataStore, + ) : ViewModel(), OnFilterClickListener, OnSearchClickListener { + private val _offerings = MutableLiveData>() + val offerings: LiveData> get() = _offerings + + val search: MutableLiveData = MutableLiveData(null) + + private val _filters: MutableLiveData> = MutableLiveData() + val filters: LiveData> get() = _filters + + val joinableFilter: LiveData = + _filters.map { + it.first { it.name == FilterName.JOINABLE } + } -class OfferingViewModel( - private val offeringRepository: OfferingRepository, - private val authRepository: AuthRepository, - private val userPreferencesDataStore: UserPreferencesDataStore, -) : ViewModel(), OnFilterClickListener, OnSearchClickListener { - private val _offerings = MutableLiveData>() - val offerings: LiveData> get() = _offerings - - val search: MutableLiveData = MutableLiveData(null) - - private val _filters: MutableLiveData> = MutableLiveData() - val filters: LiveData> get() = _filters - - val joinableFilter: LiveData = - _filters.map { - it.first { it.name == FilterName.JOINABLE } - } - - val imminentFilter: LiveData = - _filters.map { - it.first { it.name == FilterName.IMMINENT } - } + val imminentFilter: LiveData = + _filters.map { + it.first { it.name == FilterName.IMMINENT } + } - val highDiscountFilter: LiveData = - _filters.map { - it.first { it.name == FilterName.HIGH_DISCOUNT } - } + val highDiscountFilter: LiveData = + _filters.map { + it.first { it.name == FilterName.HIGH_DISCOUNT } + } - private val _selectedFilter: MutableLiveData = MutableLiveData() - val selectedFilter: LiveData get() = _selectedFilter + private val _selectedFilter: MutableLiveData = MutableLiveData() + val selectedFilter: LiveData get() = _selectedFilter - private val _searchEvent: MutableSingleLiveData = MutableSingleLiveData(null) - val searchEvent: SingleLiveData get() = _searchEvent + private val _searchEvent: MutableSingleLiveData = MutableSingleLiveData(null) + val searchEvent: SingleLiveData get() = _searchEvent - private val _filterOfferingsEvent: MutableSingleLiveData = MutableSingleLiveData() - val filterOfferingsEvent: SingleLiveData get() = _filterOfferingsEvent + private val _filterOfferingsEvent: MutableSingleLiveData = MutableSingleLiveData() + val filterOfferingsEvent: SingleLiveData get() = _filterOfferingsEvent - private val _updatedOffering: MutableSingleLiveData> = - MutableSingleLiveData(mutableListOf()) - val updatedOffering: SingleLiveData> get() = _updatedOffering + private val _updatedOffering: MutableSingleLiveData> = + MutableSingleLiveData(mutableListOf()) + val updatedOffering: SingleLiveData> get() = _updatedOffering - private val _offeringsRefreshEvent: MutableSingleLiveData = MutableSingleLiveData() - val offeringsRefreshEvent: SingleLiveData get() = _offeringsRefreshEvent + private val _offeringsRefreshEvent: MutableSingleLiveData = MutableSingleLiveData() + val offeringsRefreshEvent: SingleLiveData get() = _offeringsRefreshEvent - private val _error: MutableSingleLiveData = MutableSingleLiveData() - val error: SingleLiveData get() = _error + private val _error: MutableSingleLiveData = MutableSingleLiveData() + val error: SingleLiveData get() = _error - private val _refreshTokenExpiredEvent: MutableSingleLiveData = MutableSingleLiveData() - val refreshTokenExpiredEvent: SingleLiveData get() = _refreshTokenExpiredEvent + private val _refreshTokenExpiredEvent: MutableSingleLiveData = MutableSingleLiveData() + val refreshTokenExpiredEvent: SingleLiveData get() = _refreshTokenExpiredEvent - init { - fetchFilters() - fetchOfferings() - } + init { + fetchFilters() + fetchOfferings() + } - private fun fetchOfferings() { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = PAGE_SIZE, enablePlaceholders = false), - pagingSourceFactory = { - OfferingPagingSource( - offeringRepository, - authRepository, - search.value, - _selectedFilter.value, - ) { fetchOfferings() } - }, - ).flow.cachedIn(viewModelScope).collectLatest { pagingData -> - _offerings.value = - pagingData.map { - if (isSearchKeywordExist() && isTitleContainSearchKeyword(it)) { - return@map it.copy( - title = - highlightSearchKeyword( - it.title, - search.value!!, - ), - ) + private fun fetchOfferings() { + viewModelScope.launch { + Pager( + config = PagingConfig(pageSize = PAGE_SIZE, enablePlaceholders = false), + pagingSourceFactory = { + OfferingPagingSource( + offeringRepository, + authRepository, + search.value, + _selectedFilter.value, + ) { fetchOfferings() } + }, + ).flow.cachedIn(viewModelScope).collectLatest { pagingData -> + _offerings.value = + pagingData.map { + if (isSearchKeywordExist() && isTitleContainSearchKeyword(it)) { + return@map it.copy( + title = + highlightSearchKeyword( + it.title, + search.value!!, + ), + ) + } + it.copy(title = removeAsterisks(it.title)) } - it.copy(title = removeAsterisks(it.title)) - } + } } } - } - - private fun removeAsterisks(title: String): String { - return title.replace("*", "") - } - - private fun highlightSearchKeyword( - title: String, - keyword: String, - ): String { - return title.replace(keyword, "*$keyword*") - } - - private fun isTitleContainSearchKeyword(it: Offering) = (search.value as String) in it.title - - private fun isSearchKeywordExist() = (search.value != null) && (search.value != "") - private fun fetchFilters() { - viewModelScope.launch { - when (val result = offeringRepository.fetchFilters()) { - is Result.Error -> { - Log.d("error", "fetchFilters: ${result.error}") - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - fetchFilters() - } - - DataError.Network.FORBIDDEN -> { - userPreferencesDataStore.removeAllData() - _refreshTokenExpiredEvent.setValue(Unit) - } + private fun removeAsterisks(title: String): String { + return title.replace("*", "") + } - DataError.Network.BAD_REQUEST -> { - _error.setValue(R.string.home_filter_error_message) - } + private fun highlightSearchKeyword( + title: String, + keyword: String, + ): String { + return title.replace(keyword, "*$keyword*") + } - else -> { - Log.e("error", "fetchFilters Error: ${result.error.name}") + private fun isTitleContainSearchKeyword(it: Offering) = (search.value as String) in it.title + + private fun isSearchKeywordExist() = (search.value != null) && (search.value != "") + + private fun fetchFilters() { + viewModelScope.launch { + when (val result = offeringRepository.fetchFilters()) { + is Result.Error -> { + Log.d("error", "fetchFilters: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> fetchFilters() + is Result.Error -> { + userPreferencesDataStore.removeAllData() + _refreshTokenExpiredEvent.setValue(Unit) + return@launch + } + } + } + + DataError.Network.BAD_REQUEST -> { + _error.setValue(R.string.home_filter_error_message) + } + + else -> { + Log.e("error", "fetchFilters Error: ${result.error.name}") + } } } - } - is Result.Success -> { - _filters.value = result.data + is Result.Success -> { + _filters.value = result.data + } } } } - } - - override fun onClickFilter( - filterName: FilterName, - isChecked: Boolean, - ) { - if (isChecked) { - _selectedFilter.value = filterName.toString() - } else { - _selectedFilter.value = null - } - _filterOfferingsEvent.setValue(Unit) - fetchOfferings() - } + override fun onClickFilter( + filterName: FilterName, + isChecked: Boolean, + ) { + if (isChecked) { + _selectedFilter.value = filterName.toString() + } else { + _selectedFilter.value = null + } - override fun onClickSearchButton() { - _searchEvent.setValue(search.value) - fetchOfferings() - } + _filterOfferingsEvent.setValue(Unit) + fetchOfferings() + } - fun fetchUpdatedOffering(offeringId: Long) { - viewModelScope.launch { - when (val result = offeringRepository.fetchOffering(offeringId)) { - is Result.Error -> { - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - fetchUpdatedOffering(offeringId) - } + override fun onClickSearchButton() { + _searchEvent.setValue(search.value) + fetchOfferings() + } - DataError.Network.BAD_REQUEST -> { - _error.setValue(R.string.home_updated_offering_error_mesasge) + fun fetchUpdatedOffering(offeringId: Long) { + viewModelScope.launch { + when (val result = offeringRepository.fetchOffering(offeringId)) { + is Result.Error -> { + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> fetchUpdatedOffering(offeringId) + is Result.Error -> return@launch + } + } + + DataError.Network.BAD_REQUEST -> { + _error.setValue(R.string.home_updated_offering_error_mesasge) + } + + else -> { + Log.e("error", "fetchUpdatedOffering Error: ${result.error.name}") + } } + } - else -> { - Log.e("error", "fetchUpdatedOffering Error: ${result.error.name}") - } + is Result.Success -> { + val updatedOfferings = _updatedOffering.getValue() ?: mutableListOf() + updatedOfferings.add(result.data) + _updatedOffering.setValue(updatedOfferings) } } + } + } - is Result.Success -> { - val updatedOfferings = _updatedOffering.getValue() ?: mutableListOf() - updatedOfferings.add(result.data) - _updatedOffering.setValue(updatedOfferings) - } + fun refreshOfferings(isSuccess: Boolean) { + if (isSuccess) { + search.value = null + _selectedFilter.value = null + _offeringsRefreshEvent.setValue(Unit) + fetchOfferings() } } - } - fun refreshOfferingsByOfferingWriteEvent(isSuccess: Boolean) { - if (isSuccess) { - search.value = null - _selectedFilter.value = null + fun swipeRefresh() { _offeringsRefreshEvent.setValue(Unit) fetchOfferings() } - } - companion object { - private const val PAGE_SIZE = 10 - - @Suppress("UNCHECKED_CAST") - fun getFactory( - offeringRepository: OfferingRepository, - authRepository: AuthRepository, - userPreferencesDataStore: UserPreferencesDataStore, - ) = object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - return OfferingViewModel( - offeringRepository, - authRepository, - userPreferencesDataStore, - ) as T - } + companion object { + private const val PAGE_SIZE = 10 } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginActivity.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginActivity.kt index 450fff06e..a4e33ec8f 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginActivity.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginActivity.kt @@ -11,23 +11,17 @@ import com.kakao.sdk.auth.model.OAuthToken import com.kakao.sdk.common.model.ClientError import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient -import com.zzang.chongdae.ChongdaeApp -import com.zzang.chongdae.ChongdaeApp.Companion.dataStore -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager import com.zzang.chongdae.databinding.ActivityLoginBinding -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager import com.zzang.chongdae.presentation.view.MainActivity +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class LoginActivity : AppCompatActivity(), OnAuthClickListener { private var _binding: ActivityLoginBinding? = null private val binding get() = _binding!! - private val viewModel: LoginViewModel by viewModels { - LoginViewModel.getFactory( - authRepository = (application as ChongdaeApp).authRepository, - userPreferencesDataStore = UserPreferencesDataStore(applicationContext.dataStore), - ) - } + private val viewModel: LoginViewModel by viewModels() private val firebaseAnalytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(this) diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginViewModel.kt index c3726d2c3..00815f981 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/login/LoginViewModel.kt @@ -2,67 +2,56 @@ package com.zzang.chongdae.presentation.view.login import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore -import com.zzang.chongdae.domain.repository.AuthRepository -import com.zzang.chongdae.domain.util.Result +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.datastore.UserPreferencesDataStore +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier import com.zzang.chongdae.presentation.util.MutableSingleLiveData import com.zzang.chongdae.presentation.util.SingleLiveData +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch - -class LoginViewModel( - private val authRepository: AuthRepository, - private val userPreferencesDataStore: UserPreferencesDataStore, -) : ViewModel() { - private val _loginSuccessEvent: MutableSingleLiveData = MutableSingleLiveData() - val loginSuccessEvent: SingleLiveData get() = _loginSuccessEvent - - private val _alreadyLoggedInEvent: MutableSingleLiveData = MutableSingleLiveData() - val alreadyLoggedInEvent: SingleLiveData get() = _alreadyLoggedInEvent - - init { - makeAlreadyLoggedInEvent() - } - - private fun makeAlreadyLoggedInEvent() { - viewModelScope.launch { - val accessToken = userPreferencesDataStore.accessTokenFlow.first() - if (accessToken != null) { - _alreadyLoggedInEvent.setValue(Unit) - } +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel + @Inject + constructor( + @AuthRepositoryQualifier private val authRepository: AuthRepository, + private val userPreferencesDataStore: UserPreferencesDataStore, + ) : ViewModel() { + private val _loginSuccessEvent: MutableSingleLiveData = MutableSingleLiveData() + val loginSuccessEvent: SingleLiveData get() = _loginSuccessEvent + + private val _alreadyLoggedInEvent: MutableSingleLiveData = MutableSingleLiveData() + val alreadyLoggedInEvent: SingleLiveData get() = _alreadyLoggedInEvent + + init { + makeAlreadyLoggedInEvent() } - } - - fun postLogin(accessToken: String) { - viewModelScope.launch { - when (val result = authRepository.saveLogin(accessToken = accessToken)) { - is Result.Success -> { - userPreferencesDataStore.saveMember(result.data.memberId, result.data.nickName) - _loginSuccessEvent.setValue(Unit) - } - is Result.Error -> { - Log.e("error", "postLogin: ${result.error}") + private fun makeAlreadyLoggedInEvent() { + viewModelScope.launch { + val accessToken = userPreferencesDataStore.accessTokenFlow.first() + if (accessToken != null) { + _alreadyLoggedInEvent.setValue(Unit) } } } - } - companion object { - @Suppress("UNCHECKED_CAST") - fun getFactory( - authRepository: AuthRepository, - userPreferencesDataStore: UserPreferencesDataStore, - ) = object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - return LoginViewModel(authRepository, userPreferencesDataStore) as T + fun postLogin(accessToken: String) { + viewModelScope.launch { + when (val result = authRepository.saveLogin(accessToken = accessToken)) { + is Result.Success -> { + userPreferencesDataStore.saveMember(result.data.memberId, result.data.nickName) + _loginSuccessEvent.setValue(Unit) + } + + is Result.Error -> { + Log.e("error", "postLogin: ${result.error}") + } + } } } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageFragment.kt index c00226a20..fd6a3ac10 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageFragment.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageFragment.kt @@ -1,5 +1,6 @@ package com.zzang.chongdae.presentation.view.mypage +import android.app.Dialog import android.content.Intent import android.net.Uri import android.os.Bundle @@ -9,19 +10,24 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp.Companion.dataStore -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager +import com.zzang.chongdae.databinding.DialogAlertBinding import com.zzang.chongdae.databinding.FragmentMyPageBinding -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager import com.zzang.chongdae.presentation.view.login.LoginActivity +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MyPageFragment : Fragment() { private var _binding: FragmentMyPageBinding? = null private val binding get() = _binding!! - private val viewModel: MyPageViewModel by viewModels { - MyPageViewModel.getFactory(UserPreferencesDataStore(requireContext().dataStore)) - } + private var _alertBinding: DialogAlertBinding? = null + private val alertBinding get() = _alertBinding!! + + private val alert: Dialog by lazy { Dialog(requireContext()) } + + private val viewModel: MyPageViewModel by viewModels() private val firebaseAnalytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(requireContext()) @@ -46,6 +52,10 @@ class MyPageFragment : Fragment() { _binding = FragmentMyPageBinding.inflate(inflater, container, false) binding.vm = viewModel binding.lifecycleOwner = viewLifecycleOwner + + _alertBinding = DialogAlertBinding.inflate(inflater, container, false) + alertBinding.listener = viewModel + alertBinding.tvDialogMessage.text = getString(R.string.my_page_logout_dialog_description) } override fun onViewCreated( @@ -60,6 +70,13 @@ class MyPageFragment : Fragment() { viewModel.openUrlInBrowserEvent.observe(viewLifecycleOwner) { openUrlInBrowser(it) } + viewModel.showAlertEvent.observe(viewLifecycleOwner) { + alert.setContentView(alertBinding.root) + alert.show() + } + viewModel.alertCancelEvent.observe(viewLifecycleOwner) { + alert.dismiss() + } viewModel.logoutEvent.observe(viewLifecycleOwner) { clearDataAndLogout() } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageViewModel.kt index 8a996a133..a00cb7664 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/mypage/MyPageViewModel.kt @@ -2,59 +2,67 @@ package com.zzang.chongdae.presentation.view.mypage import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras -import com.zzang.chongdae.data.local.source.UserPreferencesDataStore +import com.zzang.chongdae.common.datastore.UserPreferencesDataStore import com.zzang.chongdae.presentation.util.MutableSingleLiveData import com.zzang.chongdae.presentation.util.SingleLiveData +import com.zzang.chongdae.presentation.view.common.OnAlertClickListener +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class MyPageViewModel(private val userPreferencesDataStore: UserPreferencesDataStore) : ViewModel() { - val nickName: LiveData = userPreferencesDataStore.nickNameFlow.asLiveData() +@HiltViewModel +class MyPageViewModel + @Inject + constructor( + private val userPreferencesDataStore: UserPreferencesDataStore, + ) : ViewModel(), + OnAlertClickListener { + val nickName: LiveData = userPreferencesDataStore.nickNameFlow.asLiveData() - private val _openUrlInBrowserEvent = MutableSingleLiveData() - val openUrlInBrowserEvent: SingleLiveData get() = _openUrlInBrowserEvent + private val _openUrlInBrowserEvent = MutableSingleLiveData() + val openUrlInBrowserEvent: SingleLiveData get() = _openUrlInBrowserEvent - private val _logoutEvent = MutableSingleLiveData() - val logoutEvent: SingleLiveData get() = _logoutEvent + private val _logoutEvent = MutableSingleLiveData() + val logoutEvent: SingleLiveData get() = _logoutEvent - private val termsOfUseUrl = - "https://silent-apparatus-578.notion.site/f1f5cd1609d4469dba3ab7d0f95c183c?pvs=4" - private val privacyUrl = - "https://silent-apparatus-578.notion.site/f1f5cd1609d4469dba3ab7d0f95c183c?pvs=4" - private val withdrawalUrl = "https://forms.gle/z5MUzVTUoyunfqEu8" + private val _showAlertEvent = MutableSingleLiveData() + val showAlertEvent: SingleLiveData get() = _showAlertEvent - fun onClickTermsOfUse() { - _openUrlInBrowserEvent.setValue(termsOfUseUrl) - } + private val _alertCancelEvent = MutableSingleLiveData() + val alertCancelEvent: SingleLiveData get() = _alertCancelEvent - fun onClickPrivacy() { - _openUrlInBrowserEvent.setValue(privacyUrl) - } + private val termsOfUseUrl = + "https://silent-apparatus-578.notion.site/f1f5cd1609d4469dba3ab7d0f95c183c?pvs=4" + private val privacyUrl = + "https://silent-apparatus-578.notion.site/f1f5cd1609d4469dba3ab7d0f95c183c?pvs=4" + private val withdrawalUrl = "https://forms.gle/z5MUzVTUoyunfqEu8" - fun onClickLogout() { - viewModelScope.launch { - userPreferencesDataStore.removeAllData() + fun onClickTermsOfUse() { + _openUrlInBrowserEvent.setValue(termsOfUseUrl) } - _logoutEvent.setValue(Unit) - } - fun onClickWithdrawal() { - _openUrlInBrowserEvent.setValue(withdrawalUrl) - } + fun onClickPrivacy() { + _openUrlInBrowserEvent.setValue(privacyUrl) + } + + fun onClickLogout() { + _showAlertEvent.setValue(Unit) + } + + fun onClickWithdrawal() { + _openUrlInBrowserEvent.setValue(withdrawalUrl) + } - companion object { - @Suppress("UNCHECKED_CAST") - fun getFactory(userPreferencesDataStore: UserPreferencesDataStore) = - object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - return MyPageViewModel(userPreferencesDataStore) as T - } + override fun onClickConfirm() { + viewModelScope.launch { + userPreferencesDataStore.removeAllData() } + _logoutEvent.setValue(Unit) + } + + override fun onClickCancel() { + _alertCancelEvent.setValue(Unit) + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailFragment.kt index 1801288be..30d0cbaaf 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailFragment.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailFragment.kt @@ -1,5 +1,6 @@ package com.zzang.chongdae.presentation.view.offeringdetail +import android.app.Dialog import android.content.Intent import android.net.Uri import android.os.Bundle @@ -14,26 +15,35 @@ import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp +import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager +import com.zzang.chongdae.databinding.DialogAlertBinding +import com.zzang.chongdae.databinding.DialogDeleteOfferingBinding import com.zzang.chongdae.databinding.FragmentOfferingDetailBinding -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager import com.zzang.chongdae.presentation.view.MainActivity import com.zzang.chongdae.presentation.view.commentdetail.CommentDetailActivity import com.zzang.chongdae.presentation.view.home.HomeFragment +import com.zzang.chongdae.presentation.view.login.LoginActivity +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject -class OfferingDetailFragment : Fragment() { +@AndroidEntryPoint +class OfferingDetailFragment : Fragment(), OnOfferingDeleteAlertClickListener { private var _binding: FragmentOfferingDetailBinding? = null private val binding get() = _binding!! private var toast: Toast? = null private val offeringId by lazy { - arguments?.getLong(HomeFragment.OFFERING_ID) - ?: throw IllegalArgumentException() + arguments?.getLong(HomeFragment.OFFERING_ID) ?: throw IllegalArgumentException() } + private val dialog: Dialog by lazy { Dialog(requireContext()) } + + @Inject + lateinit var offeringDetailAssistedFactory: OfferingDetailViewModel.OfferingDetailAssistedFactory + private val viewModel: OfferingDetailViewModel by viewModels { OfferingDetailViewModel.getFactory( + assistedFactory = offeringDetailAssistedFactory, offeringId = offeringId, - offeringDetailRepository = (requireActivity().application as ChongdaeApp).offeringDetailRepository, - authRepository = (requireActivity().applicationContext as ChongdaeApp).authRepository, ) } @@ -64,6 +74,11 @@ class OfferingDetailFragment : Fragment() { setUpObserve() } + override fun onResume() { + super.onResume() + viewModel.loadOffering() + } + private fun setUpObserve() { viewModel.updatedOfferingId.observe(viewLifecycleOwner) { setFragmentResult(OFFERING_DETAIL_BUNDLE_KEY, bundleOf(UPDATED_OFFERING_ID_KEY to it)) @@ -80,6 +95,64 @@ class OfferingDetailFragment : Fragment() { viewModel.productLinkRedirectEvent.observe(viewLifecycleOwner) { productURL -> openUrlInBrowser(productURL) } + + viewModel.modifyOfferingEvent.observe(viewLifecycleOwner) { + findNavController().navigate( + R.id.action_offering_detail_fragment_to_offering_modify_essential_fragment, + bundleOf(HomeFragment.OFFERING_ID to offeringId), + ) + } + + viewModel.deleteOfferingEvent.observe(viewLifecycleOwner) { + showUpdateStatusDialog() + } + + viewModel.deleteOfferingSuccessEvent.observe(viewLifecycleOwner) { + dialog.dismiss() + findNavController().popBackStack() + setFragmentResult(OFFERING_DETAIL_BUNDLE_KEY, bundleOf(DELETED_OFFERING_ID_KEY to true)) + showToast(R.string.offering_detail_delete_complete_message) + } + + viewModel.showAlertEvent.observe(viewLifecycleOwner) { + val alertBinding = DialogAlertBinding.inflate(layoutInflater, null, false) + alertBinding.tvDialogMessage.text = getString(R.string.offering_detail_participate_alert) + alertBinding.listener = viewModel + + dialog.setContentView(alertBinding.root) + dialog.show() + } + + viewModel.alertCancelEvent.observe(viewLifecycleOwner) { + dialog.dismiss() + } + } + + override fun onClickConfirm() { + viewModel.deleteOffering(offeringId) + firebaseAnalyticsManager.logSelectContentEvent( + id = "delete_offering_event", + name = "delete_offering_event", + contentType = "button", + ) + } + + override fun onClickCancel() { + firebaseAnalyticsManager.logSelectContentEvent( + id = "cancel_delete_offering_event", + name = "cancel_delete_offering_event", + contentType = "button", + ) + dialog.dismiss() + } + + private fun showUpdateStatusDialog() { + val dialogBinding = DialogDeleteOfferingBinding.inflate(layoutInflater, null, false) + + dialogBinding.listener = this + + dialog.setContentView(dialogBinding.root) + dialog.show() } private fun openUrlInBrowser(url: String) { @@ -105,7 +178,7 @@ class OfferingDetailFragment : Fragment() { } private fun setUpMoveCommentDetailEventObserve() { - viewModel.commentDetailEvent.observe(this) { + viewModel.commentDetailEvent.observe(viewLifecycleOwner) { firebaseAnalyticsManager.logSelectContentEvent( id = "Offering_Item_ID: $offeringId", name = "participate_offering_event", @@ -113,6 +186,11 @@ class OfferingDetailFragment : Fragment() { ) findNavController().popBackStack() CommentDetailActivity.startActivity(requireContext(), offeringId) + dialog.dismiss() + } + + viewModel.refreshTokenExpiredEvent.observe(viewLifecycleOwner) { + LoginActivity.startActivity(requireContext()) } } @@ -132,5 +210,7 @@ class OfferingDetailFragment : Fragment() { companion object { const val OFFERING_DETAIL_BUNDLE_KEY = "offering_detail_bundle_key" const val UPDATED_OFFERING_ID_KEY = "updated_offering_id" + const val DELETED_OFFERING_ID_KEY = "deleted_offering_id" + const val OFFERING_ID_KEY = "offering_id" } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailViewModel.kt index cf65a7a1a..21455ec1e 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OfferingDetailViewModel.kt @@ -6,161 +6,255 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import com.zzang.chongdae.R +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.datastore.UserPreferencesDataStore +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import com.zzang.chongdae.di.annotations.OfferingDetailRepositoryQualifier import com.zzang.chongdae.domain.model.OfferingCondition import com.zzang.chongdae.domain.model.OfferingCondition.Companion.isAvailable import com.zzang.chongdae.domain.model.OfferingDetail -import com.zzang.chongdae.domain.repository.AuthRepository import com.zzang.chongdae.domain.repository.OfferingDetailRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result import com.zzang.chongdae.presentation.util.MutableSingleLiveData import com.zzang.chongdae.presentation.util.SingleLiveData +import com.zzang.chongdae.presentation.view.common.OnAlertClickListener +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.launch -class OfferingDetailViewModel( - private val offeringId: Long, - private val offeringDetailRepository: OfferingDetailRepository, - private val authRepository: AuthRepository, -) : ViewModel(), - OnParticipationClickListener, - OnOfferingReportClickListener, - OnMoveCommentDetailClickListener, - OnProductLinkClickListener { - private val _offeringDetail: MutableLiveData = MutableLiveData() - val offeringDetail: LiveData get() = _offeringDetail +class OfferingDetailViewModel + @AssistedInject + constructor( + @Assisted private val offeringId: Long, + @OfferingDetailRepositoryQualifier val offeringDetailRepository: OfferingDetailRepository, + @AuthRepositoryQualifier private val authRepository: AuthRepository, + private val userPreferencesDataStore: UserPreferencesDataStore, + ) : ViewModel(), + OnParticipationClickListener, + OnOfferingReportClickListener, + OnMoveCommentDetailClickListener, + OnProductLinkClickListener, + OnOfferingModifyClickListener, + OnAlertClickListener { + @AssistedFactory + interface OfferingDetailAssistedFactory { + fun create(offeringId: Long): OfferingDetailViewModel + } - private val _currentCount: MutableLiveData = MutableLiveData() - val currentCount: LiveData get() = _currentCount + private val _offeringDetail: MutableLiveData = MutableLiveData() + val offeringDetail: LiveData get() = _offeringDetail - private val _offeringCondition: MutableLiveData = MutableLiveData() - val offeringCondition: LiveData get() = _offeringCondition + private val _currentCount: MutableLiveData = MutableLiveData() + val currentCount: LiveData get() = _currentCount - private val _isParticipated: MutableLiveData = MutableLiveData(false) - val isParticipated: LiveData get() = _isParticipated + private val _offeringCondition: MutableLiveData = MutableLiveData() + val offeringCondition: LiveData get() = _offeringCondition - private val _isParticipationAvailable: MutableLiveData = MutableLiveData(true) - val isParticipationAvailable: LiveData get() = _isParticipationAvailable + private val _isParticipated: MutableLiveData = MutableLiveData(false) + val isParticipated: LiveData get() = _isParticipated - private val _isRepresentative: MutableLiveData = MutableLiveData(true) - val isRepresentative: LiveData get() = _isRepresentative + private val _isParticipationAvailable: MutableLiveData = MutableLiveData(true) + val isParticipationAvailable: LiveData get() = _isParticipationAvailable - private val _commentDetailEvent: MutableSingleLiveData = MutableSingleLiveData() - val commentDetailEvent: SingleLiveData get() = _commentDetailEvent + private val _isRepresentative: MutableLiveData = MutableLiveData(true) + val isRepresentative: LiveData get() = _isRepresentative - private val _updatedOfferingId: MutableLiveData = MutableLiveData() - val updatedOfferingId: LiveData get() = _updatedOfferingId + private val _commentDetailEvent: MutableSingleLiveData = MutableSingleLiveData() + val commentDetailEvent: SingleLiveData get() = _commentDetailEvent - private val _reportEvent: MutableSingleLiveData = MutableSingleLiveData() - val reportEvent: SingleLiveData get() = _reportEvent + private val _updatedOfferingId: MutableLiveData = MutableLiveData() + val updatedOfferingId: LiveData get() = _updatedOfferingId - private val _productLinkRedirectEvent: MutableSingleLiveData = MutableSingleLiveData() - val productLinkRedirectEvent: SingleLiveData get() = _productLinkRedirectEvent + private val _reportEvent: MutableSingleLiveData = MutableSingleLiveData() + val reportEvent: SingleLiveData get() = _reportEvent - private val _error: MutableSingleLiveData = MutableSingleLiveData() - val error: SingleLiveData get() = _error + private val _productLinkRedirectEvent: MutableSingleLiveData = MutableSingleLiveData() + val productLinkRedirectEvent: SingleLiveData get() = _productLinkRedirectEvent - init { - loadOffering() - } + private val _error: MutableSingleLiveData = MutableSingleLiveData() + val error: SingleLiveData get() = _error - private fun loadOffering() { - viewModelScope.launch { - when (val result = offeringDetailRepository.fetchOfferingDetail(offeringId)) { - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - loadOffering() - } + private val _modifyOfferingEvent: MutableSingleLiveData = MutableSingleLiveData() + val modifyOfferingEvent: SingleLiveData get() = _modifyOfferingEvent - DataError.Network.BAD_REQUEST -> { - _error.setValue(R.string.offering_detail_load_error_mesage) - } + private val _deleteOfferingEvent: MutableSingleLiveData = MutableSingleLiveData() + val deleteOfferingEvent: SingleLiveData get() = _deleteOfferingEvent + + private val _deleteOfferingSuccessEvent: MutableSingleLiveData = MutableSingleLiveData() + val deleteOfferingSuccessEvent: SingleLiveData get() = _deleteOfferingSuccessEvent + + private val _refreshTokenExpiredEvent: MutableSingleLiveData = MutableSingleLiveData() + val refreshTokenExpiredEvent: SingleLiveData get() = _refreshTokenExpiredEvent + + private val _showAlertEvent = MutableSingleLiveData() + val showAlertEvent: SingleLiveData get() = _showAlertEvent - else -> { - Log.e("error", "loadOffering Error: ${result.error.name}") + private val _alertCancelEvent = MutableSingleLiveData() + val alertCancelEvent: SingleLiveData get() = _alertCancelEvent + + init { + loadOffering() + } + + fun loadOffering() { + viewModelScope.launch { + when (val result = offeringDetailRepository.fetchOfferingDetail(offeringId)) { + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> loadOffering() + is Result.Error -> { + userPreferencesDataStore.removeAllData() + _refreshTokenExpiredEvent.setValue(Unit) + return@launch + } + } + } + + DataError.Network.BAD_REQUEST -> { + _error.setValue(R.string.offering_detail_load_error_mesage) + } + + else -> { + Log.e("error", "loadOffering Error: ${result.error.name}") + } } - } - is Result.Success -> { - _offeringDetail.value = result.data - _currentCount.value = result.data.currentCount.value - _offeringCondition.value = result.data.condition - _isParticipated.value = result.data.isParticipated - _isParticipationAvailable.value = - isParticipationEnabled(result.data.condition, result.data.isParticipated) - _isRepresentative.value = result.data.isProposer + is Result.Success -> { + _offeringDetail.value = result.data + _currentCount.value = result.data.currentCount.value + _offeringCondition.value = result.data.condition + _isParticipated.value = result.data.isParticipated + _isParticipationAvailable.value = + isParticipationEnabled(result.data.condition, result.data.isParticipated) + _isRepresentative.value = result.data.isProposer + } } } } - } - override fun onClickParticipation() { - viewModelScope.launch { - when (val result = offeringDetailRepository.saveParticipation(offeringId)) { - is Result.Error -> - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - onClickParticipation() - } + override fun participate() { + viewModelScope.launch { + when (val result = offeringDetailRepository.saveParticipation(offeringId)) { + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> participate() + is Result.Error -> { + userPreferencesDataStore.removeAllData() + _refreshTokenExpiredEvent.setValue(Unit) + return@launch + } + } + } - DataError.Network.BAD_REQUEST -> { - _error.setValue(R.string.offering_detail_participation_error) - } + DataError.Network.BAD_REQUEST -> { + _error.setValue(R.string.offering_detail_participation_error) + } - else -> { - Log.e("error", "onClickParticipation Error: ${result.error.name}") + else -> { + Log.e("error", "onClickParticipation Error: ${result.error.name}") + } } - } - is Result.Success -> { - _isParticipated.value = true - _commentDetailEvent.setValue(offeringDetail.value?.title ?: DEFAULT_TITLE) - _updatedOfferingId.value = offeringId + is Result.Success -> { + _isParticipated.value = true + _commentDetailEvent.setValue(offeringDetail.value?.title ?: DEFAULT_TITLE) + _updatedOfferingId.value = offeringId + } } } } - } - override fun onClickMoveCommentDetail() { - _commentDetailEvent.setValue(offeringDetail.value?.title ?: DEFAULT_TITLE) - } + override fun onClickMoveCommentDetail() { + _commentDetailEvent.setValue(offeringDetail.value?.title ?: DEFAULT_TITLE) + } - override fun onClickReport() { - _reportEvent.setValue(R.string.report_url) - } + override fun onClickReport() { + _reportEvent.setValue(R.string.report_url) + } - override fun onClickProductRedirectText(productUrl: String) { - _productLinkRedirectEvent.setValue(productUrl) - } + override fun onClickProductRedirectText(productUrl: String) { + _productLinkRedirectEvent.setValue(productUrl) + } + + override fun onClickOfferingModify() { + if (_offeringCondition.value == OfferingCondition.CONFIRMED) { + _error.setValue(R.string.error_modify_invalid) + return + } + _modifyOfferingEvent.setValue(offeringId) + } + + fun onClickDeleteButton() { + _deleteOfferingEvent.setValue(Unit) + } + + fun deleteOffering(offeringId: Long) { + viewModelScope.launch { + when (val result = offeringDetailRepository.deleteOffering(offeringId)) { + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> deleteOffering(offeringId) + is Result.Error -> return@launch + } + } - private fun isParticipationEnabled( - offeringCondition: OfferingCondition, - isParticipated: Boolean, - ) = !isParticipated && offeringCondition.isAvailable() - - companion object { - private const val DEFAULT_TITLE = "" - - @Suppress("UNCHECKED_CAST") - fun getFactory( - offeringId: Long, - offeringDetailRepository: OfferingDetailRepository, - authRepository: AuthRepository, - ) = object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - return OfferingDetailViewModel( - offeringId, - offeringDetailRepository, - authRepository, - ) as T + DataError.Network.NULL -> { + _deleteOfferingSuccessEvent.setValue(Unit) + } + + DataError.Network.BAD_REQUEST -> { + _error.setValue(R.string.offering_detail_delete_error) + } + + else -> { + Log.e("error", "onClickOfferingDelete Error: ${result.error.name}") + } + } + + is Result.Success -> { + _deleteOfferingSuccessEvent.setValue(Unit) + } + } } } + + private fun isParticipationEnabled( + offeringCondition: OfferingCondition, + isParticipated: Boolean, + ) = !isParticipated && offeringCondition.isAvailable() + + companion object { + private const val DEFAULT_TITLE = "" + + @Suppress("UNCHECKED_CAST") + fun getFactory( + assistedFactory: OfferingDetailAssistedFactory, + offeringId: Long, + ) = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return assistedFactory.create(offeringId) as T + } + } + } + + fun onParticipateClick() { + _showAlertEvent.setValue(Unit) + } + + override fun onClickConfirm() { + participate() + } + + override fun onClickCancel() { + _alertCancelEvent.setValue(Unit) + } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingDeleteAlertClickListener.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingDeleteAlertClickListener.kt new file mode 100644 index 000000000..7c3be57b2 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingDeleteAlertClickListener.kt @@ -0,0 +1,7 @@ +package com.zzang.chongdae.presentation.view.offeringdetail + +interface OnOfferingDeleteAlertClickListener { + fun onClickConfirm() + + fun onClickCancel() +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingModifyClickListener.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingModifyClickListener.kt new file mode 100644 index 000000000..45cbd285c --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnOfferingModifyClickListener.kt @@ -0,0 +1,5 @@ +package com.zzang.chongdae.presentation.view.offeringdetail + +fun interface OnOfferingModifyClickListener { + fun onClickOfferingModify() +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnParticipationClickListener.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnParticipationClickListener.kt index 9858c47fa..26302cd49 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnParticipationClickListener.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringdetail/OnParticipationClickListener.kt @@ -1,5 +1,5 @@ package com.zzang.chongdae.presentation.view.offeringdetail interface OnParticipationClickListener { - fun onClickParticipation() + fun participate() } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/ModifyUIState.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/ModifyUIState.kt new file mode 100644 index 000000000..3efb039d9 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/ModifyUIState.kt @@ -0,0 +1,24 @@ +package com.zzang.chongdae.presentation.view.offeringmodify + +import androidx.annotation.StringRes + +sealed class ModifyUIState { + data class Empty( + @StringRes val message: Int, + ) : ModifyUIState() + + data object Initial : ModifyUIState() + + data object Loading : ModifyUIState() + + data class InvalidInput( + @StringRes val message: Int, + ) : ModifyUIState() + + data class Success(val url: String) : ModifyUIState() + + data class Error( + @StringRes val message: Int, + val errorMessage: String, + ) : ModifyUIState() +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyEssentialFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyEssentialFragment.kt new file mode 100644 index 000000000..7ea58f1ac --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyEssentialFragment.kt @@ -0,0 +1,225 @@ +package com.zzang.chongdae.presentation.view.offeringmodify + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.setFragmentResultListener +import androidx.navigation.fragment.findNavController +import com.google.firebase.analytics.FirebaseAnalytics +import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager +import com.zzang.chongdae.databinding.DialogDatePickerBinding +import com.zzang.chongdae.databinding.FragmentOfferingModifyEssentialBinding +import com.zzang.chongdae.presentation.util.setDebouncedOnClickListener +import com.zzang.chongdae.presentation.view.MainActivity +import com.zzang.chongdae.presentation.view.address.AddressFinderDialog +import com.zzang.chongdae.presentation.view.home.HomeFragment +import com.zzang.chongdae.presentation.view.write.OnDateTimeButtonsClickListener +import dagger.hilt.android.AndroidEntryPoint +import java.util.Calendar + +@AndroidEntryPoint +class OfferingModifyEssentialFragment : Fragment(), OnDateTimeButtonsClickListener { + private var _fragmentBinding: FragmentOfferingModifyEssentialBinding? = null + private val fragmentBinding get() = _fragmentBinding!! + + private var _dateTimePickerBinding: DialogDatePickerBinding? = null + private val dateTimePickerBinding get() = _dateTimePickerBinding!! + + private var toast: Toast? = null + private val dialog: Dialog by lazy { Dialog(requireActivity()) } + + private val offeringId by lazy { + arguments?.getLong(HomeFragment.OFFERING_ID) ?: throw IllegalArgumentException() + } + + private val viewModel: OfferingModifyViewModel by activityViewModels() + + private val firebaseAnalytics: FirebaseAnalytics by lazy { + FirebaseAnalytics.getInstance(requireContext()) + } + + private val firebaseAnalyticsManager: FirebaseAnalyticsManager by lazy { + FirebaseAnalyticsManager(firebaseAnalytics) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.initOfferingId(offeringId) + viewModel.fetchOfferingDetail() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + initBinding(inflater, container) + return fragmentBinding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + (activity as MainActivity).hideBottomNavigation() + setUpObserve() + selectMeetingDate() + searchPlace() + } + + private fun setUpObserve() { + observeNavigateToOptionalEvent() + observeUIState() + } + + private fun observeUIState() { + viewModel.modifyUIState.observe(viewLifecycleOwner) { state -> + when (state) { + is ModifyUIState.Error -> { + showToast(state.message) + } + + is ModifyUIState.Empty -> { + showToast(state.message) + } + + is ModifyUIState.InvalidInput -> { + showToast(state.message) + } + + else -> {} + } + } + } + + private fun searchPlace() { + fragmentBinding.tvPlaceValue.setDebouncedOnClickListener(800L) { + AddressFinderDialog().show(parentFragmentManager, this.tag) + } + setFragmentResultListener(AddressFinderDialog.ADDRESS_KEY) { _, bundle -> + fragmentBinding.tvPlaceValue.text = + bundle.getString(AddressFinderDialog.BUNDLE_ADDRESS_KEY) + } + } + + private fun selectMeetingDate() { + viewModel.meetingDateChoiceEvent.observe(viewLifecycleOwner) { + dialog.setContentView(dateTimePickerBinding.root) + dialog.show() + setDateTimeText(dateTimePickerBinding) + } + } + + private fun setDateTimeText(dateTimeBinding: DialogDatePickerBinding) { + val calendar = Calendar.getInstance() + updateDate(calendar, dateTimeBinding) + } + + private fun updateDate( + calendar: Calendar, + dateTimeBinding: DialogDatePickerBinding, + ) { + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + updateDateTextView(dateTimeBinding.tvDate, year, month, day) + dateTimeBinding.pickerDate.minDate = System.currentTimeMillis() + dateTimeBinding.pickerDate.setOnDateChangedListener { _, year, monthOfYear, dayOfMonth -> + updateDateTextView(dateTimeBinding.tvDate, year, monthOfYear, dayOfMonth) + } + } + + override fun onDateTimeSubmitButtonClick() { + viewModel.updateMeetingDate( + dateTimePickerBinding.tvDate.text.toString(), + ) + dialog.dismiss() + } + + override fun onDateTimeCancelButtonClick() { + dialog.dismiss() + } + + private fun updateDateTextView( + textView: TextView, + year: Int, + monthOfYear: Int, + dayOfMonth: Int, + ) { + textView.text = + getString(R.string.write_selected_date).format( + year, + monthOfYear + 1, + dayOfMonth, + ) + } + + private fun updateTimeTextView( + textView: TextView, + hourOfDay: Int, + minute: Int, + ) { + val amPm = if (hourOfDay < 12) getString(R.string.all_am) else getString(R.string.all_pm) + val hour = if (hourOfDay % 12 == 0) 12 else hourOfDay % 12 + textView.text = getString(R.string.write_selected_time, amPm, hour, minute) + } + + private fun initBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ) { + _fragmentBinding = + FragmentOfferingModifyEssentialBinding.inflate(inflater, container, false) + fragmentBinding.vm = viewModel + fragmentBinding.lifecycleOwner = viewLifecycleOwner + + _dateTimePickerBinding = DialogDatePickerBinding.inflate(inflater, container, false) + dateTimePickerBinding.onClickListener = this + } + + private fun observeNavigateToOptionalEvent() { + viewModel.navigateToOptionalEvent.observe(viewLifecycleOwner) { + firebaseAnalyticsManager.logSelectContentEvent( + id = "submit_offering_write_essential_event", + name = "submit_offering_write_essential_event", + contentType = "button", + ) + findNavController().navigate(R.id.action_offering_modify_essential_fragment_to_offering_modify_optional_fragment) + } + } + + private fun showToast( + @StringRes messageId: Int, + ) { + toast?.cancel() + toast = + Toast.makeText( + requireActivity(), + getString(messageId), + Toast.LENGTH_SHORT, + ) + toast?.show() + } + + override fun onResume() { + super.onResume() + firebaseAnalyticsManager.logScreenView( + screenName = "OfferingWriteEssentialFragment", + screenClass = this::class.java.simpleName, + ) + } + + override fun onDestroy() { + super.onDestroy() + _fragmentBinding = null + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyOptionalFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyOptionalFragment.kt new file mode 100644 index 000000000..c8f026384 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyOptionalFragment.kt @@ -0,0 +1,192 @@ +package com.zzang.chongdae.presentation.view.offeringmodify + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.setFragmentResult +import androidx.navigation.fragment.findNavController +import com.google.firebase.analytics.FirebaseAnalytics +import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager +import com.zzang.chongdae.databinding.FragmentOfferingModifyOptionalBinding +import com.zzang.chongdae.presentation.util.FileUtils +import com.zzang.chongdae.presentation.util.PermissionManager + +class OfferingModifyOptionalFragment : Fragment() { + private var _fragmentBinding: FragmentOfferingModifyOptionalBinding? = null + private val fragmentBinding get() = _fragmentBinding!! + + private var toast: Toast? = null + + private lateinit var permissionManager: PermissionManager + private lateinit var pickMediaLauncher: ActivityResultLauncher + + private val viewModel: OfferingModifyViewModel by activityViewModels() + + private val firebaseAnalytics: FirebaseAnalytics by lazy { + FirebaseAnalytics.getInstance(requireContext()) + } + + private val firebaseAnalyticsManager: FirebaseAnalyticsManager by lazy { + FirebaseAnalyticsManager(firebaseAnalytics) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpPermissionManager() + initializePhotoPicker() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + initBinding(inflater, container) + return fragmentBinding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + setUpObserve() + } + + private fun initBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ) { + _fragmentBinding = FragmentOfferingModifyOptionalBinding.inflate(inflater, container, false) + fragmentBinding.vm = viewModel + fragmentBinding.lifecycleOwner = viewLifecycleOwner + } + + private fun observeSubmitOfferingEvent() { + viewModel.submitOfferingModifyEvent.observe(viewLifecycleOwner) { + firebaseAnalyticsManager.logSelectContentEvent( + id = "submit_offering_event", + name = "submit_offering_event", + contentType = "button", + ) + showToast(R.string.modify_success_modifing) + findNavController().popBackStack(R.id.offering_modify_essential_fragment, true) + viewModel.initOfferingModifyInputs() + + setFragmentResult( + OFFERING_WRITE_BUNDLE_KEY, + bundleOf(NEW_OFFERING_EVENT_KEY to true), + ) + } + } + + private fun setUpObserve() { + observeUIState() + observeSubmitOfferingEvent() + observeImageUploadEvent() + } + + private fun showToast( + @StringRes messageId: Int, + ) { + toast?.cancel() + toast = + Toast.makeText( + requireActivity(), + getString(messageId), + Toast.LENGTH_SHORT, + ) + toast?.show() + } + + private fun initializePhotoPicker() { + pickMediaLauncher = + registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? -> + handleMediaResult(uri) + } + } + + private fun launchPhotoPicker() { + pickMediaLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + + private fun handleMediaResult(uri: Uri?) { + if (uri != null) { + val multipartBodyPart = FileUtils.getMultipartBodyPart(requireContext(), uri, "image") + if (multipartBodyPart != null) { + viewModel.uploadImageFile(multipartBodyPart) + } else { + showToast(R.string.all_error_file_conversion) + } + } + } + + private fun setUpPermissionManager() { + permissionManager = + PermissionManager( + fragment = this, + onPermissionGranted = { onPermissionsGranted() }, + onPermissionDenied = { onPermissionsDenied() }, + ) + } + + private fun observeImageUploadEvent() { + viewModel.imageUploadEvent.observe(viewLifecycleOwner) { + if (permissionManager.isAndroid13OrAbove()) { + launchPhotoPicker() + } else { + permissionManager.requestPermissions() + } + } + } + + private fun observeUIState() { + viewModel.modifyUIState.observe(viewLifecycleOwner) { state -> + when (state) { + is ModifyUIState.Error -> { + showToast(state.message) + } + + is ModifyUIState.Empty -> { + showToast(state.message) + } + + is ModifyUIState.InvalidInput -> { + showToast(state.message) + } + + else -> {} + } + } + } + + private fun onPermissionsGranted() { + showToast(R.string.all_permission_granted) + launchPhotoPicker() + } + + private fun onPermissionsDenied() { + showToast(R.string.all_permission_denied) + } + + override fun onDestroy() { + super.onDestroy() + _fragmentBinding = null + } + + companion object { + const val OFFERING_WRITE_BUNDLE_KEY = "offering_write_bundle_key" + const val NEW_OFFERING_EVENT_KEY = "new_offering_event_key" + } +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyViewModel.kt new file mode 100644 index 000000000..62465d018 --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/offeringmodify/OfferingModifyViewModel.kt @@ -0,0 +1,489 @@ +package com.zzang.chongdae.presentation.view.offeringmodify + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import androidx.lifecycle.viewModelScope +import com.zzang.chongdae.R +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import com.zzang.chongdae.di.annotations.OfferingDetailRepositoryQualifier +import com.zzang.chongdae.di.annotations.OfferingRepositoryQualifier +import com.zzang.chongdae.domain.model.Count +import com.zzang.chongdae.domain.model.DiscountPrice +import com.zzang.chongdae.domain.model.OfferingDetail +import com.zzang.chongdae.domain.model.OfferingModifyDomainRequest +import com.zzang.chongdae.domain.model.Price +import com.zzang.chongdae.domain.repository.OfferingDetailRepository +import com.zzang.chongdae.domain.repository.OfferingRepository +import com.zzang.chongdae.presentation.util.MutableSingleLiveData +import com.zzang.chongdae.presentation.util.SingleLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import okhttp3.MultipartBody +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale +import javax.inject.Inject + +@HiltViewModel +class OfferingModifyViewModel + @Inject + constructor( + @OfferingRepositoryQualifier private val offeringRepository: OfferingRepository, + @OfferingDetailRepositoryQualifier private val offeringDetailRepository: OfferingDetailRepository, + @AuthRepositoryQualifier private val authRepository: AuthRepository, + ) : ViewModel() { + private var offeringId: Long = DEFAULT_OFFERING_ID + + val title: MutableLiveData = MutableLiveData("") + + val productUrl: MutableLiveData = MutableLiveData(null) + + val thumbnailUrl: MutableLiveData = MutableLiveData("") + + val deleteImageVisible: LiveData = thumbnailUrl.map { !it.isNullOrBlank() } + + val totalCount: MutableLiveData = MutableLiveData("$MINIMUM_TOTAL_COUNT") + + val totalPrice: MutableLiveData = MutableLiveData("") + + val originPrice: MutableLiveData = MutableLiveData("") + + val meetingAddress: MutableLiveData = MutableLiveData("") + + val meetingAddressDetail: MutableLiveData = MutableLiveData("") + + val meetingDate: MutableLiveData = MutableLiveData("") + + private val meetingDateValue: MutableLiveData = MutableLiveData("") + + val description: MutableLiveData = MutableLiveData("") + + val descriptionLength: LiveData + get() = description.map { it.length } + + private val _essentialSubmitButtonEnabled: MediatorLiveData = MediatorLiveData(false) + val essentialSubmitButtonEnabled: LiveData get() = _essentialSubmitButtonEnabled + + private val _extractButtonEnabled: MediatorLiveData = MediatorLiveData(false) + val extractButtonEnabled: LiveData get() = _extractButtonEnabled + + private val _splitPrice: MediatorLiveData = MediatorLiveData(ERROR_INTEGER_FORMAT) + val splitPrice: LiveData get() = _splitPrice + + private val _discountRate: MediatorLiveData = MediatorLiveData(ERROR_FLOAT_FORMAT) + val splitPriceValidity: LiveData + get() = _splitPrice.map { it >= 0 } + + val discountRateValidity: LiveData + get() = _discountRate.map { it >= 0 } + + val discountRate: LiveData get() = _discountRate + + private val _meetingDateChoiceEvent: MutableSingleLiveData = MutableSingleLiveData() + val meetingDateChoiceEvent: SingleLiveData get() = _meetingDateChoiceEvent + + private val _navigateToOptionalEvent: MutableSingleLiveData = MutableSingleLiveData() + val navigateToOptionalEvent: SingleLiveData get() = _navigateToOptionalEvent + + private val _submitOfferingModifyEvent: MutableSingleLiveData = MutableSingleLiveData() + val submitOfferingModifyEvent: SingleLiveData get() = _submitOfferingModifyEvent + + private val _imageUploadEvent = MutableLiveData() + val imageUploadEvent: LiveData get() = _imageUploadEvent + + private val _modifyUIState = MutableLiveData(ModifyUIState.Initial) + val modifyUIState: LiveData get() = _modifyUIState + + val isLoading: LiveData = _modifyUIState.map { it is ModifyUIState.Loading } + + init { + + _essentialSubmitButtonEnabled.apply { + addSource(title) { updateSubmitButtonEnabled() } + addSource(totalCount) { updateSubmitButtonEnabled() } + addSource(totalPrice) { updateSubmitButtonEnabled() } + addSource(meetingAddress) { updateSubmitButtonEnabled() } + addSource(meetingDate) { updateSubmitButtonEnabled() } + } + + _splitPrice.apply { + addSource(totalCount) { safeUpdateSplitPrice() } + addSource(totalPrice) { safeUpdateSplitPrice() } + } + + _discountRate.apply { + addSource(_splitPrice) { safeUpdateDiscountRate() } + addSource(originPrice) { safeUpdateDiscountRate() } + } + + _extractButtonEnabled.apply { + addSource(productUrl) { value = !productUrl.value.isNullOrBlank() } + } + } + + fun initOfferingId(id: Long) { + offeringId = id + } + + private fun safeUpdateSplitPrice() { + runCatching { + updateSplitPrice() + }.onFailure { + _splitPrice.value = ERROR_INTEGER_FORMAT + } + } + + fun clearProductUrl() { + productUrl.value = null + } + + fun onUploadPhotoClick() { + _imageUploadEvent.value = Unit + } + + fun uploadImageFile(multipartBody: MultipartBody.Part) { + viewModelScope.launch { + _modifyUIState.value = ModifyUIState.Loading + when (val result = offeringRepository.saveProductImageS3(multipartBody)) { + is Result.Success -> { + _modifyUIState.value = ModifyUIState.Success(result.data.imageUrl) + thumbnailUrl.value = result.data.imageUrl + } + + is Result.Error -> { + Log.e("error", "uploadImageFile: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> uploadImageFile(multipartBody) + is Result.Error -> return@launch + } + } + + else -> { + _modifyUIState.value = + ModifyUIState.Error( + R.string.all_error_image_upload, + "${result.error}", + ) + } + } + } + } + } + } + + fun postProductImageOg() { + viewModelScope.launch { + _modifyUIState.value = ModifyUIState.Loading + when (val result = offeringRepository.saveProductImageOg(productUrl.value ?: "")) { + is Result.Success -> { + if (result.data.imageUrl.isBlank()) { + _modifyUIState.value = ModifyUIState.Empty(R.string.error_empty_product_url) + return@launch + } + _modifyUIState.value = ModifyUIState.Success(result.data.imageUrl) + thumbnailUrl.value = HTTPS + result.data.imageUrl + } + + is Result.Error -> { + Log.e("error", "postProductImageOg: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> postProductImageOg() + is Result.Error -> return@launch + } + } + + else -> { + _modifyUIState.value = + ModifyUIState.Error( + R.string.error_invalid_product_url, + "${result.error}", + ) + } + } + } + } + } + } + + fun clearProductImage() { + thumbnailUrl.value = null + } + + private fun safeUpdateDiscountRate() { + runCatching { + updateDiscountRate() + }.onFailure { + _discountRate.value = ERROR_FLOAT_FORMAT + } + } + + private fun updateSubmitButtonEnabled() { + _essentialSubmitButtonEnabled.value = !title.value.isNullOrBlank() && + !totalCount.value.isNullOrBlank() && + !totalPrice.value.isNullOrBlank() && + !meetingAddress.value.isNullOrBlank() && + !meetingDate.value.isNullOrBlank() + } + + private fun updateSplitPrice() { + val totalPrice = Price.fromString(totalPrice.value) + val totalCount = Count.fromString(totalCount.value) + _splitPrice.value = totalPrice.amount / totalCount.number + } + + private fun updateDiscountRate() { + val originPrice = Price.fromString(originPrice.value) + val splitPrice = Price.fromInteger(_splitPrice.value) + val discountPriceValue = originPrice.amount - splitPrice.amount + val discountPrice = DiscountPrice.fromFloat(discountPriceValue.toFloat()) + _discountRate.value = (discountPrice.amount / originPrice.amount) * 100 + } + + fun increaseTotalCount() { + val totalCount = Count.fromString(totalCount.value).increase() + this.totalCount.value = totalCount.number.toString() + } + + fun decreaseTotalCount() { + if (Count.fromString(totalCount.value).number < 0) { + this.totalCount.value = MINIMUM_TOTAL_COUNT.toString() + return + } + val totalCount = Count.fromString(totalCount.value).decrease() + this.totalCount.value = totalCount.number.toString() + } + + fun makeMeetingDateChoiceEvent() { + _meetingDateChoiceEvent.setValue(true) + } + + fun updateMeetingDate(date: String) { + val dateTime = "$date" + val inputFormat = SimpleDateFormat(DATE_FORMAT_DOMAIN, Locale.KOREAN) + val outputFormat = SimpleDateFormat(DATE_TIME_FORMAT_REMOTE_WITH_SEC, Locale.getDefault()) + + val parsedDateTime = inputFormat.parse(dateTime) + meetingDateValue.value = parsedDateTime?.let { outputFormat.format(it) } + meetingDate.value = dateTime + } + + private fun initDateTimeWhenModify(dateTimeString: String) { + val inputFormatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_REMOTE) + val outputFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT_DOMAIN) + val inputFormatterWithSec = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_REMOTE_WITH_SEC) + val dateTime = LocalDateTime.parse(dateTimeString, inputFormatter) + meetingDate.value = dateTime.format(outputFormatter) + meetingDateValue.value = dateTime.format(inputFormatterWithSec) + } + + fun fetchOfferingDetail() { + viewModelScope.launch { + when (val result = offeringDetailRepository.fetchOfferingDetail(offeringId)) { + is Result.Success -> { + initExistingOffering(result.data) + } + + is Result.Error -> + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> fetchOfferingDetail() + is Result.Error -> return@launch + } + } + + else -> { + Log.e("error", "loadOffering Error: ${result.error.name}") + } + } + } + } + } + + private fun initExistingOffering(offeringDetail: OfferingDetail) { + title.value = offeringDetail.title + productUrl.value = offeringDetail.productUrl + thumbnailUrl.value = offeringDetail.thumbnailUrl + totalCount.value = offeringDetail.totalCount.toString() + totalPrice.value = offeringDetail.totalPrice.toString() + originPrice.value = offeringDetail.originPrice?.toString() ?: "" + meetingAddress.value = offeringDetail.meetingAddress + meetingAddressDetail.value = offeringDetail.meetingAddressDetail + initDateTimeWhenModify(offeringDetail.meetingDate.toString()) + description.value = offeringDetail.description + } + + fun postOfferingModify() { + val title = title.value ?: return + val totalCount = totalCount.value ?: return + val totalPrice = totalPrice.value ?: return + val meetingAddress = meetingAddress.value ?: return + val meetingAddressDetail = meetingAddressDetail.value ?: return + val meetingDate = meetingDateValue.value ?: return + val description = description.value ?: return + + val totalCountConverted = makeTotalCountInvalidEvent(totalCount) ?: return + val totalPriceConverted = makeTotalPriceInvalidEvent(totalPrice) ?: return + val meetingAddressDong = extractDong(meetingAddress) + + var originPriceNotBlank: Int? = 0 + runCatching { + originPriceNotBlank = originPriceToPositiveIntOrNull(originPrice.value) + }.onFailure { + makeOriginPriceInvalidEvent() + return + } + if (isOriginPriceCheaperThanSplitPriceEvent()) return + + viewModelScope.launch { + when ( + val result = + offeringRepository.patchOffering( + offeringId = offeringId, + offeringModifyDomainRequest = + OfferingModifyDomainRequest( + title = title, + productUrl = productUrlOrNull(), + thumbnailUrl = thumbnailUrl.value, + totalCount = totalCountConverted, + totalPrice = totalPriceConverted, + originPrice = originPriceNotBlank, + meetingAddress = meetingAddress, + meetingAddressDong = meetingAddressDong, + meetingAddressDetail = meetingAddressDetail, + meetingDate = meetingDate, + description = description, + ), + ) + ) { + is Result.Success -> { + makeSubmitOfferingModifyEvent() + } + + is Result.Error -> { + Log.e("error", "postOffering: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> postOfferingModify() + is Result.Error -> return@launch + } + } + + else -> { + _modifyUIState.value = + ModifyUIState.Error( + R.string.modify_error_modifing, + "${result.error}", + ) + } + } + } + } + } + } + + private fun productUrlOrNull(): String? { + val productUrl = productUrl.value + if (productUrl == "") return null + return productUrl + } + + private fun originPriceToPositiveIntOrNull(input: String?): Int? { + val originPriceInputTrim = input?.trim() + if (originPriceInputTrim.isNullOrBlank()) { + return null + } + if (originPriceInputTrim.toInt() < 0) { + throw NumberFormatException() + } + return originPriceInputTrim.toInt() + } + + private fun extractDong(address: String): String? { + val regex = """\((.*?)\)""".toRegex() + val matchResult = regex.find(address) + val content = matchResult?.groups?.get(1)?.value + return content?.split(",")?.get(0)?.trim() + } + + private fun makeTotalCountInvalidEvent(totalCount: String): Int? { + val totalCountValue = totalCount.trim().toIntOrNull() ?: ERROR_INTEGER_FORMAT + if (totalCountValue < MINIMUM_TOTAL_COUNT || totalCountValue > MAXIMUM_TOTAL_COUNT) { + _modifyUIState.value = ModifyUIState.InvalidInput(R.string.write_invalid_total_count) + return null + } + return totalCountValue + } + + private fun makeTotalPriceInvalidEvent(totalPrice: String): Int? { + val totalPriceConverted = totalPrice.trim().toIntOrNull() ?: ERROR_INTEGER_FORMAT + if (totalPriceConverted < 0) { + _modifyUIState.value = ModifyUIState.InvalidInput(R.string.write_invalid_total_price) + return null + } + return totalPriceConverted + } + + private fun makeOriginPriceInvalidEvent() { + _modifyUIState.value = ModifyUIState.InvalidInput(R.string.write_invalid_origin_price) + } + + private fun isOriginPriceCheaperThanSplitPriceEvent(): Boolean { + if (originPrice.value.isNullOrBlank()) return false + val discountRateValue = discountRate.value ?: ERROR_FLOAT_FORMAT + if (discountRateValue <= 0f) { + _modifyUIState.value = + ModifyUIState.InvalidInput(R.string.write_origin_price_cheaper_than_total_price) + return true + } + return false + } + + fun makeNavigateToOptionalEvent() { + _navigateToOptionalEvent.setValue(true) + } + + private fun makeSubmitOfferingModifyEvent() { + _submitOfferingModifyEvent.setValue(Unit) + } + + fun initOfferingModifyInputs() { + title.value = "" + productUrl.value = "" + thumbnailUrl.value = "" + totalCount.value = "$MINIMUM_TOTAL_COUNT" + totalPrice.value = "" + originPrice.value = "" + meetingAddress.value = "" + meetingAddressDetail.value = "" + meetingDate.value = "" + meetingDateValue.value = "" + description.value = "" + } + + companion object { + private const val DEFAULT_OFFERING_ID = 0L + private const val ERROR_INTEGER_FORMAT = -1 + private const val ERROR_FLOAT_FORMAT = -1f + private const val MINIMUM_TOTAL_COUNT = 2 + private const val MAXIMUM_TOTAL_COUNT = 10_000 + private const val INPUT_DATE_TIME_FORMAT = "yyyy년 M월 d일 a h시 m분" + private const val DATE_FORMAT_DOMAIN = "yyyy년 M월 d일" + private const val DATE_TIME_FORMAT_REMOTE = "yyyy-MM-dd'T'HH:mm" + private const val DATE_TIME_FORMAT_REMOTE_WITH_SEC = "yyyy-MM-dd'T'HH:mm:ss" + const val HTTPS = "https:" + } + } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteEssentialFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteEssentialFragment.kt index cf5f17c68..803fa9603 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteEssentialFragment.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteEssentialFragment.kt @@ -13,16 +13,18 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResultListener import androidx.navigation.fragment.findNavController import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager import com.zzang.chongdae.databinding.DialogDatePickerBinding import com.zzang.chongdae.databinding.FragmentOfferingWriteEssentialBinding -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager +import com.zzang.chongdae.presentation.util.setDebouncedOnClickListener import com.zzang.chongdae.presentation.view.MainActivity import com.zzang.chongdae.presentation.view.address.AddressFinderDialog +import dagger.hilt.android.AndroidEntryPoint import java.util.Calendar -class OfferingWriteEssentialFragment : Fragment(), OnOfferingWriteClickListener { +@AndroidEntryPoint +class OfferingWriteEssentialFragment : Fragment(), OnDateTimeButtonsClickListener { private var _fragmentBinding: FragmentOfferingWriteEssentialBinding? = null private val fragmentBinding get() = _fragmentBinding!! @@ -32,12 +34,7 @@ class OfferingWriteEssentialFragment : Fragment(), OnOfferingWriteClickListener private var toast: Toast? = null private val dialog: Dialog by lazy { Dialog(requireActivity()) } - private val viewModel: OfferingWriteViewModel by activityViewModels { - OfferingWriteViewModel.getFactory( - offeringRepository = (requireActivity().application as ChongdaeApp).offeringRepository, - authRepository = (requireActivity().application as ChongdaeApp).authRepository, - ) - } + private val viewModel: OfferingWriteViewModel by activityViewModels() private val firebaseAnalytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(requireContext()) @@ -90,7 +87,7 @@ class OfferingWriteEssentialFragment : Fragment(), OnOfferingWriteClickListener } private fun searchPlace() { - fragmentBinding.tvPlaceValue.setOnClickListener { + fragmentBinding.tvPlaceValue.setDebouncedOnClickListener(800L) { AddressFinderDialog().show(parentFragmentManager, this.tag) } setFragmentResultListener(AddressFinderDialog.ADDRESS_KEY) { _, bundle -> @@ -120,6 +117,7 @@ class OfferingWriteEssentialFragment : Fragment(), OnOfferingWriteClickListener val month = calendar.get(Calendar.MONTH) val day = calendar.get(Calendar.DAY_OF_MONTH) updateDateTextView(dateTimeBinding.tvDate, year, month, day) + dateTimeBinding.pickerDate.minDate = System.currentTimeMillis() dateTimeBinding.pickerDate.setOnDateChangedListener { _, year, monthOfYear, dayOfMonth -> updateDateTextView(dateTimeBinding.tvDate, year, monthOfYear, dayOfMonth) } @@ -169,7 +167,6 @@ class OfferingWriteEssentialFragment : Fragment(), OnOfferingWriteClickListener fragmentBinding.lifecycleOwner = viewLifecycleOwner _dateTimePickerBinding = DialogDatePickerBinding.inflate(inflater, container, false) - dateTimePickerBinding.vm = viewModel dateTimePickerBinding.onClickListener = this } @@ -208,5 +205,6 @@ class OfferingWriteEssentialFragment : Fragment(), OnOfferingWriteClickListener override fun onDestroy() { super.onDestroy() _fragmentBinding = null + viewModel.initOfferingWriteInputs() } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteOptionalFragment.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteOptionalFragment.kt index a9b3a67dc..241bcac57 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteOptionalFragment.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteOptionalFragment.kt @@ -16,13 +16,14 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResult import androidx.navigation.fragment.findNavController import com.google.firebase.analytics.FirebaseAnalytics -import com.zzang.chongdae.ChongdaeApp import com.zzang.chongdae.R +import com.zzang.chongdae.common.firebase.FirebaseAnalyticsManager import com.zzang.chongdae.databinding.FragmentOfferingWriteOptionalBinding import com.zzang.chongdae.presentation.util.FileUtils -import com.zzang.chongdae.presentation.util.FirebaseAnalyticsManager import com.zzang.chongdae.presentation.util.PermissionManager +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class OfferingWriteOptionalFragment : Fragment() { private var _fragmentBinding: FragmentOfferingWriteOptionalBinding? = null private val fragmentBinding get() = _fragmentBinding!! @@ -32,12 +33,7 @@ class OfferingWriteOptionalFragment : Fragment() { private lateinit var permissionManager: PermissionManager private lateinit var pickMediaLauncher: ActivityResultLauncher - private val viewModel: OfferingWriteViewModel by activityViewModels { - OfferingWriteViewModel.getFactory( - offeringRepository = (requireActivity().application as ChongdaeApp).offeringRepository, - authRepository = (requireActivity().application as ChongdaeApp).authRepository, - ) - } + private val viewModel: OfferingWriteViewModel by activityViewModels() private val firebaseAnalytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(requireContext()) @@ -87,9 +83,7 @@ class OfferingWriteOptionalFragment : Fragment() { contentType = "button", ) showToast(R.string.write_success_writing) - findNavController().popBackStack(R.id.offering_write_fragment_essential, true) - viewModel.initOfferingWriteInputs() - + findNavController().popBackStack(R.id.offering_write_essential_fragment, true) setFragmentResult( OFFERING_WRITE_BUNDLE_KEY, bundleOf(NEW_OFFERING_EVENT_KEY to true), diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteViewModel.kt index 304f2f8d9..c8b337b7b 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OfferingWriteViewModel.kt @@ -5,402 +5,418 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import com.zzang.chongdae.R +import com.zzang.chongdae.auth.repository.AuthRepository +import com.zzang.chongdae.common.handler.DataError +import com.zzang.chongdae.common.handler.Result +import com.zzang.chongdae.di.annotations.AuthRepositoryQualifier +import com.zzang.chongdae.di.annotations.OfferingRepositoryQualifier import com.zzang.chongdae.domain.model.Count import com.zzang.chongdae.domain.model.DiscountPrice +import com.zzang.chongdae.domain.model.OfferingWrite import com.zzang.chongdae.domain.model.Price -import com.zzang.chongdae.domain.repository.AuthRepository import com.zzang.chongdae.domain.repository.OfferingRepository -import com.zzang.chongdae.domain.util.DataError -import com.zzang.chongdae.domain.util.Result import com.zzang.chongdae.presentation.util.MutableSingleLiveData import com.zzang.chongdae.presentation.util.SingleLiveData +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import okhttp3.MultipartBody import java.text.SimpleDateFormat import java.util.Locale +import javax.inject.Inject -class OfferingWriteViewModel( - private val offeringRepository: OfferingRepository, - private val authRepository: AuthRepository, -) : ViewModel() { - val title: MutableLiveData = MutableLiveData("") +@HiltViewModel +class OfferingWriteViewModel + @Inject + constructor( + @OfferingRepositoryQualifier private val offeringRepository: OfferingRepository, + @AuthRepositoryQualifier private val authRepository: AuthRepository, + ) : ViewModel() { + val title: MutableLiveData = MutableLiveData("") - val productUrl: MutableLiveData = MutableLiveData(null) + val productUrl: MutableLiveData = MutableLiveData(null) - val thumbnailUrl: MutableLiveData = MutableLiveData("") + val thumbnailUrl: MutableLiveData = MutableLiveData("") - val deleteImageVisible: LiveData = thumbnailUrl.map { !it.isNullOrBlank() } + val deleteImageVisible: LiveData = thumbnailUrl.map { !it.isNullOrBlank() } - val totalCount: MutableLiveData = MutableLiveData("$MINIMUM_TOTAL_COUNT") + val totalCount: MutableLiveData = MutableLiveData("$MINIMUM_TOTAL_COUNT") - val totalPrice: MutableLiveData = MutableLiveData("") + val totalPrice: MutableLiveData = MutableLiveData("") - val originPrice: MutableLiveData = MutableLiveData("") + val originPrice: MutableLiveData = MutableLiveData("") - val meetingAddress: MutableLiveData = MutableLiveData("") + val meetingAddress: MutableLiveData = MutableLiveData("") - val meetingAddressDetail: MutableLiveData = MutableLiveData("") + val meetingAddressDetail: MutableLiveData = MutableLiveData("") - val meetingDate: MutableLiveData = MutableLiveData("") + val meetingDate: MutableLiveData = MutableLiveData("") - private val meetingDateValue: MutableLiveData = MutableLiveData("") + private val meetingDateValue: MutableLiveData = MutableLiveData("") - val description: MutableLiveData = MutableLiveData("") + val description: MutableLiveData = MutableLiveData("") - val descriptionLength: LiveData - get() = description.map { it.length } + val descriptionLength: LiveData + get() = description.map { it.length } - private val _essentialSubmitButtonEnabled: MediatorLiveData = MediatorLiveData(false) - val essentialSubmitButtonEnabled: LiveData get() = _essentialSubmitButtonEnabled + private val _essentialSubmitButtonEnabled: MediatorLiveData = MediatorLiveData(false) + val essentialSubmitButtonEnabled: LiveData get() = _essentialSubmitButtonEnabled - private val _extractButtonEnabled: MediatorLiveData = MediatorLiveData(false) - val extractButtonEnabled: LiveData get() = _extractButtonEnabled + private val _extractButtonEnabled: MediatorLiveData = MediatorLiveData(false) + val extractButtonEnabled: LiveData get() = _extractButtonEnabled - private val _splitPrice: MediatorLiveData = MediatorLiveData(ERROR_INTEGER_FORMAT) - val splitPrice: LiveData get() = _splitPrice + private val _splitPrice: MediatorLiveData = MediatorLiveData(ERROR_INTEGER_FORMAT) + val splitPrice: LiveData get() = _splitPrice - private val _discountRate: MediatorLiveData = MediatorLiveData(ERROR_FLOAT_FORMAT) - val splitPriceValidity: LiveData - get() = _splitPrice.map { it >= 0 } + private val _discountRate: MediatorLiveData = MediatorLiveData(ERROR_FLOAT_FORMAT) + val splitPriceValidity: LiveData + get() = _splitPrice.map { it >= 0 } - val discountRateValidity: LiveData - get() = _discountRate.map { it >= 0 } + val discountRateValidity: LiveData + get() = _discountRate.map { it >= 0 } - val discountRate: LiveData get() = _discountRate + val discountRate: LiveData get() = _discountRate - private val _meetingDateChoiceEvent: MutableSingleLiveData = MutableSingleLiveData() - val meetingDateChoiceEvent: SingleLiveData get() = _meetingDateChoiceEvent + private val _meetingDateChoiceEvent: MutableSingleLiveData = MutableSingleLiveData() + val meetingDateChoiceEvent: SingleLiveData get() = _meetingDateChoiceEvent - private val _navigateToOptionalEvent: MutableSingleLiveData = MutableSingleLiveData() - val navigateToOptionalEvent: SingleLiveData get() = _navigateToOptionalEvent + private val _navigateToOptionalEvent: MutableSingleLiveData = MutableSingleLiveData() + val navigateToOptionalEvent: SingleLiveData get() = _navigateToOptionalEvent - private val _submitOfferingEvent: MutableSingleLiveData = MutableSingleLiveData() - val submitOfferingEvent: SingleLiveData get() = _submitOfferingEvent + private val _submitOfferingEvent: MutableSingleLiveData = MutableSingleLiveData() + val submitOfferingEvent: SingleLiveData get() = _submitOfferingEvent - private val _imageUploadEvent = MutableLiveData() - val imageUploadEvent: LiveData get() = _imageUploadEvent + private val _imageUploadEvent = MutableLiveData() + val imageUploadEvent: LiveData get() = _imageUploadEvent - private val _writeUIState = MutableLiveData(WriteUIState.Initial) - val writeUIState: LiveData get() = _writeUIState + private val _writeUIState = MutableLiveData(WriteUIState.Initial) + val writeUIState: LiveData get() = _writeUIState - val isLoading: LiveData = _writeUIState.map { it is WriteUIState.Loading } + val isLoading: LiveData = _writeUIState.map { it is WriteUIState.Loading } - init { - _essentialSubmitButtonEnabled.apply { - addSource(title) { updateSubmitButtonEnabled() } - addSource(totalCount) { updateSubmitButtonEnabled() } - addSource(totalPrice) { updateSubmitButtonEnabled() } - addSource(meetingAddress) { updateSubmitButtonEnabled() } - addSource(meetingDate) { updateSubmitButtonEnabled() } - } + init { + _essentialSubmitButtonEnabled.apply { + addSource(title) { updateSubmitButtonEnabled() } + addSource(totalCount) { updateSubmitButtonEnabled() } + addSource(totalPrice) { updateSubmitButtonEnabled() } + addSource(meetingAddress) { updateSubmitButtonEnabled() } + addSource(meetingDate) { updateSubmitButtonEnabled() } + } - _splitPrice.apply { - addSource(totalCount) { safeUpdateSplitPrice() } - addSource(totalPrice) { safeUpdateSplitPrice() } - } + _splitPrice.apply { + addSource(totalCount) { safeUpdateSplitPrice() } + addSource(totalPrice) { safeUpdateSplitPrice() } + } - _discountRate.apply { - addSource(_splitPrice) { safeUpdateDiscountRate() } - addSource(originPrice) { safeUpdateDiscountRate() } - } + _discountRate.apply { + addSource(_splitPrice) { safeUpdateDiscountRate() } + addSource(originPrice) { safeUpdateDiscountRate() } + } - _extractButtonEnabled.apply { - addSource(productUrl) { value = !productUrl.value.isNullOrBlank() } + _extractButtonEnabled.apply { + addSource(productUrl) { value = !productUrl.value.isNullOrBlank() } + } } - } - private fun safeUpdateSplitPrice() { - runCatching { - updateSplitPrice() - }.onFailure { - _splitPrice.value = ERROR_INTEGER_FORMAT + private fun safeUpdateSplitPrice() { + runCatching { + updateSplitPrice() + }.onFailure { + _splitPrice.value = ERROR_INTEGER_FORMAT + } } - } - fun clearProductUrl() { - productUrl.value = null - } + fun clearProductUrl() { + productUrl.value = null + } - fun onUploadPhotoClick() { - _imageUploadEvent.value = Unit - } + fun onUploadPhotoClick() { + _imageUploadEvent.value = Unit + } - fun uploadImageFile(multipartBody: MultipartBody.Part) { - viewModelScope.launch { - when (val result = offeringRepository.saveProductImageS3(multipartBody)) { - is Result.Success -> { - _writeUIState.value = WriteUIState.Success(result.data.imageUrl) - thumbnailUrl.value = result.data.imageUrl - } + fun uploadImageFile(multipartBody: MultipartBody.Part) { + viewModelScope.launch { + _writeUIState.value = WriteUIState.Loading + when (val result = offeringRepository.saveProductImageS3(multipartBody)) { + is Result.Success -> { + _writeUIState.value = WriteUIState.Success(result.data.imageUrl) + thumbnailUrl.value = result.data.imageUrl + } - is Result.Error -> { - Log.e("error", "uploadImageFile: ${result.error}") - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - uploadImageFile(multipartBody) + is Result.Error -> { + Log.e("error", "uploadImageFile: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> uploadImageFile(multipartBody) + is Result.Error -> return@launch + } + } + + else -> { + _writeUIState.value = + WriteUIState.Error( + R.string.all_error_image_upload, + "${result.error}", + ) + } } - else -> {} } } } } - } - fun postProductImageOg() { - viewModelScope.launch { - when (val result = offeringRepository.saveProductImageOg(productUrl.value ?: "")) { - is Result.Success -> { - if (result.data.imageUrl.isBlank()) { - _writeUIState.value = WriteUIState.Empty(R.string.error_empty_product_url) - return@launch - } - thumbnailUrl.value = HTTPS + result.data.imageUrl - } - - is Result.Error -> { - Log.e("error", "postProductImageOg: ${result.error}") - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - postProductImageOg() + fun postProductImageOg() { + viewModelScope.launch { + _writeUIState.value = WriteUIState.Loading + when (val result = offeringRepository.saveProductImageOg(productUrl.value ?: "")) { + is Result.Success -> { + if (result.data.imageUrl.isBlank()) { + _writeUIState.value = WriteUIState.Empty(R.string.error_empty_product_url) + return@launch } + _writeUIState.value = WriteUIState.Success(result.data.imageUrl) + thumbnailUrl.value = HTTPS + result.data.imageUrl + } - else -> { - _writeUIState.value = - WriteUIState.Error(R.string.error_invalid_product_url, "${result.error}") + is Result.Error -> { + Log.e("error", "postProductImageOg: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> postProductImageOg() + is Result.Error -> return@launch + } + } + + else -> { + _writeUIState.value = + WriteUIState.Error( + R.string.error_invalid_product_url, + "${result.error}", + ) + } } } } } } - } - - fun clearProductImage() { - thumbnailUrl.value = null - } - private fun safeUpdateDiscountRate() { - runCatching { - updateDiscountRate() - }.onFailure { - _discountRate.value = ERROR_FLOAT_FORMAT + fun clearProductImage() { + thumbnailUrl.value = null } - } - private fun updateSubmitButtonEnabled() { - _essentialSubmitButtonEnabled.value = !title.value.isNullOrBlank() && - !totalCount.value.isNullOrBlank() && - !totalPrice.value.isNullOrBlank() && - !meetingAddress.value.isNullOrBlank() && - !meetingDate.value.isNullOrBlank() - } + private fun safeUpdateDiscountRate() { + runCatching { + updateDiscountRate() + }.onFailure { + _discountRate.value = ERROR_FLOAT_FORMAT + } + } - private fun updateSplitPrice() { - val totalPrice = Price.fromString(totalPrice.value) - val totalCount = Count.fromString(totalCount.value) - _splitPrice.value = totalPrice.amount / totalCount.number - } + private fun updateSubmitButtonEnabled() { + _essentialSubmitButtonEnabled.value = !title.value.isNullOrBlank() && + !totalCount.value.isNullOrBlank() && + !totalPrice.value.isNullOrBlank() && + !meetingAddress.value.isNullOrBlank() && + !meetingDate.value.isNullOrBlank() + } - private fun updateDiscountRate() { - val originPrice = Price.fromString(originPrice.value) - val splitPrice = Price.fromInteger(_splitPrice.value) - val discountPriceValue = originPrice.amount - splitPrice.amount - val discountPrice = DiscountPrice.fromFloat(discountPriceValue.toFloat()) - _discountRate.value = (discountPrice.amount / originPrice.amount) * 100 - } + private fun updateSplitPrice() { + val totalPrice = Price.fromString(totalPrice.value) + val totalCount = Count.fromString(totalCount.value) + _splitPrice.value = totalPrice.amount / totalCount.number + } - fun increaseTotalCount() { - val totalCount = Count.fromString(totalCount.value).increase() - this.totalCount.value = totalCount.number.toString() - } + private fun updateDiscountRate() { + val originPrice = Price.fromString(originPrice.value) + val splitPrice = Price.fromInteger(_splitPrice.value) + val discountPriceValue = originPrice.amount - splitPrice.amount + val discountPrice = DiscountPrice.fromFloat(discountPriceValue.toFloat()) + _discountRate.value = (discountPrice.amount / originPrice.amount) * 100 + } - fun decreaseTotalCount() { - if (Count.fromString(totalCount.value).number < 0) { - this.totalCount.value = MINIMUM_TOTAL_COUNT.toString() - return + fun increaseTotalCount() { + val totalCount = Count.fromString(totalCount.value).increase() + this.totalCount.value = totalCount.number.toString() } - val totalCount = Count.fromString(totalCount.value).decrease() - this.totalCount.value = totalCount.number.toString() - } - fun makeMeetingDateChoiceEvent() { - _meetingDateChoiceEvent.setValue(true) - } + fun decreaseTotalCount() { + if (Count.fromString(totalCount.value).number < 0) { + this.totalCount.value = MINIMUM_TOTAL_COUNT.toString() + return + } + val totalCount = Count.fromString(totalCount.value).decrease() + this.totalCount.value = totalCount.number.toString() + } - fun updateMeetingDate(date: String) { - val dateTime = "$date" - val inputFormat = SimpleDateFormat(INPUT_DATE_FORMAT, Locale.KOREAN) - val outputFormat = SimpleDateFormat(OUTPUT_DATE_TIME_FORMAT, Locale.getDefault()) + fun makeMeetingDateChoiceEvent() { + _meetingDateChoiceEvent.setValue(true) + } - val parsedDateTime = inputFormat.parse(dateTime) - meetingDateValue.value = parsedDateTime?.let { outputFormat.format(it) } - meetingDate.value = dateTime - } + fun updateMeetingDate(date: String) { + val dateTime = "$date" + val inputFormat = SimpleDateFormat(INPUT_DATE_FORMAT, Locale.KOREAN) + val outputFormat = SimpleDateFormat(OUTPUT_DATE_TIME_FORMAT, Locale.getDefault()) - fun postOffering() { - val title = title.value ?: return - val totalCount = totalCount.value ?: return - val totalPrice = totalPrice.value ?: return - val meetingAddress = meetingAddress.value ?: return - val meetingAddressDetail = meetingAddressDetail.value ?: return - val meetingDate = meetingDateValue.value ?: return - val description = description.value ?: return - - val totalCountConverted = makeTotalCountInvalidEvent(totalCount) ?: return - val totalPriceConverted = makeTotalPriceInvalidEvent(totalPrice) ?: return - val meetingAddressDong = extractDong(meetingAddress) - - var originPriceNotBlank: Int? = 0 - runCatching { - originPriceNotBlank = originPriceToPositiveIntOrNull(originPrice.value) - }.onFailure { - makeOriginPriceInvalidEvent() - return + val parsedDateTime = inputFormat.parse(dateTime) + meetingDateValue.value = parsedDateTime?.let { outputFormat.format(it) } + meetingDate.value = dateTime } - if (isOriginPriceCheaperThanSplitPriceEvent()) return - - viewModelScope.launch { - when ( - val result = - offeringRepository.saveOffering( - uiModel = - OfferingWriteUiModel( - title = title, - productUrl = productUrl.value, - thumbnailUrl = thumbnailUrl.value, - totalCount = totalCountConverted, - totalPrice = totalPriceConverted, - originPrice = originPriceNotBlank, - meetingAddress = meetingAddress, - meetingAddressDong = meetingAddressDong, - meetingAddressDetail = meetingAddressDetail, - meetingDate = meetingDate, - description = description, - ), - ) - ) { - is Result.Success -> makeSubmitOfferingEvent() - - is Result.Error -> { - Log.e("error", "postOffering: ${result.error}") - when (result.error) { - DataError.Network.UNAUTHORIZED -> { - authRepository.saveRefresh() - postOffering() - } - else -> { - _writeUIState.value = - WriteUIState.Error(R.string.write_error_writing, "${result.error}") + + fun postOffering() { + val title = title.value ?: return + val totalCount = totalCount.value ?: return + val totalPrice = totalPrice.value ?: return + val meetingAddress = meetingAddress.value ?: return + val meetingAddressDetail = meetingAddressDetail.value ?: return + val meetingDate = meetingDateValue.value ?: return + val description = description.value ?: return + + val totalCountConverted = makeTotalCountInvalidEvent(totalCount) ?: return + val totalPriceConverted = makeTotalPriceInvalidEvent(totalPrice) ?: return + val meetingAddressDong = extractDong(meetingAddress) + + var originPriceNotBlank: Int? = 0 + runCatching { + originPriceNotBlank = originPriceToPositiveIntOrNull(originPrice.value) + }.onFailure { + makeOriginPriceInvalidEvent() + return + } + if (isOriginPriceCheaperThanSplitPriceEvent()) return + + viewModelScope.launch { + when ( + val result = + offeringRepository.saveOffering( + offeringWrite = + OfferingWrite( + title = title, + productUrl = productUrlOrNull(), + thumbnailUrl = thumbnailUrl.value, + totalCount = totalCountConverted, + totalPrice = totalPriceConverted, + originPrice = originPriceNotBlank, + meetingAddress = meetingAddress, + meetingAddressDong = meetingAddressDong, + meetingAddressDetail = meetingAddressDetail, + meetingDate = meetingDate, + description = description, + ), + ) + ) { + is Result.Success -> makeSubmitOfferingEvent() + + is Result.Error -> { + Log.e("error", "postOffering: ${result.error}") + when (result.error) { + DataError.Network.UNAUTHORIZED -> { + when (authRepository.saveRefresh()) { + is Result.Success -> postOffering() + is Result.Error -> return@launch + } + } + + else -> { + _writeUIState.value = + WriteUIState.Error(R.string.write_error_writing, "${result.error}") + } } } } } } - } - private fun originPriceToPositiveIntOrNull(input: String?): Int? { - val originPriceInputTrim = input?.trim() - if (originPriceInputTrim.isNullOrBlank()) { - return null + private fun productUrlOrNull(): String? { + val productUrl = productUrl.value + if (productUrl == "") return null + return productUrl } - if (originPriceInputTrim.toInt() < 0) { - throw NumberFormatException() + + private fun originPriceToPositiveIntOrNull(input: String?): Int? { + val originPriceInputTrim = input?.trim() + if (originPriceInputTrim.isNullOrBlank()) { + return null + } + if (originPriceInputTrim.toInt() < 0) { + throw NumberFormatException() + } + return originPriceInputTrim.toInt() } - return originPriceInputTrim.toInt() - } - private fun extractDong(address: String): String? { - val regex = """\((.*?)\)""".toRegex() - val matchResult = regex.find(address) - val content = matchResult?.groups?.get(1)?.value - return content?.split(",")?.get(0)?.trim() - } + private fun extractDong(address: String): String? { + val regex = """\((.*?)\)""".toRegex() + val matchResult = regex.find(address) + val content = matchResult?.groups?.get(1)?.value + return content?.split(",")?.get(0)?.trim() + } - private fun makeTotalCountInvalidEvent(totalCount: String): Int? { - val totalCountValue = totalCount.trim().toIntOrNull() ?: ERROR_INTEGER_FORMAT - if (totalCountValue < MINIMUM_TOTAL_COUNT || totalCountValue > MAXIMUM_TOTAL_COUNT) { - _writeUIState.value = WriteUIState.InvalidInput(R.string.write_invalid_total_count) - return null + private fun makeTotalCountInvalidEvent(totalCount: String): Int? { + val totalCountValue = totalCount.trim().toIntOrNull() ?: ERROR_INTEGER_FORMAT + if (totalCountValue < MINIMUM_TOTAL_COUNT || totalCountValue > MAXIMUM_TOTAL_COUNT) { + _writeUIState.value = WriteUIState.InvalidInput(R.string.write_invalid_total_count) + return null + } + return totalCountValue } - return totalCountValue - } - private fun makeTotalPriceInvalidEvent(totalPrice: String): Int? { - val totalPriceConverted = totalPrice.trim().toIntOrNull() ?: ERROR_INTEGER_FORMAT - if (totalPriceConverted < 0) { - _writeUIState.value = WriteUIState.InvalidInput(R.string.write_invalid_total_price) - return null + private fun makeTotalPriceInvalidEvent(totalPrice: String): Int? { + val totalPriceConverted = totalPrice.trim().toIntOrNull() ?: ERROR_INTEGER_FORMAT + if (totalPriceConverted < 0) { + _writeUIState.value = WriteUIState.InvalidInput(R.string.write_invalid_total_price) + return null + } + return totalPriceConverted } - return totalPriceConverted - } - private fun makeOriginPriceInvalidEvent() { - _writeUIState.value = WriteUIState.InvalidInput(R.string.write_invalid_origin_price) - } + private fun makeOriginPriceInvalidEvent() { + _writeUIState.value = WriteUIState.InvalidInput(R.string.write_invalid_origin_price) + } - private fun isOriginPriceCheaperThanSplitPriceEvent(): Boolean { - if (originPrice.value.isNullOrBlank()) return false - val discountRateValue = discountRate.value ?: ERROR_FLOAT_FORMAT - if (discountRateValue <= 0f) { - _writeUIState.value = - WriteUIState.InvalidInput(R.string.write_origin_price_cheaper_than_total_price) - return true + private fun isOriginPriceCheaperThanSplitPriceEvent(): Boolean { + if (originPrice.value.isNullOrBlank()) return false + val discountRateValue = discountRate.value ?: ERROR_FLOAT_FORMAT + if (discountRateValue <= 0f) { + _writeUIState.value = + WriteUIState.InvalidInput(R.string.write_origin_price_cheaper_than_total_price) + return true + } + return false } - return false - } - fun makeNavigateToOptionalEvent() { - _navigateToOptionalEvent.setValue(true) - } + fun makeNavigateToOptionalEvent() { + _navigateToOptionalEvent.setValue(true) + } - private fun makeSubmitOfferingEvent() { - _submitOfferingEvent.setValue(Unit) - } + private fun makeSubmitOfferingEvent() { + _submitOfferingEvent.setValue(Unit) + } - fun initOfferingWriteInputs() { - title.value = "" - productUrl.value = "" - thumbnailUrl.value = "" - totalCount.value = "$MINIMUM_TOTAL_COUNT" - totalPrice.value = "" - originPrice.value = "" - meetingAddress.value = "" - meetingAddressDetail.value = "" - meetingDate.value = "" - meetingDateValue.value = "" - description.value = "" - } + fun initOfferingWriteInputs() { + title.value = "" + productUrl.value = "" + thumbnailUrl.value = "" + totalCount.value = "$MINIMUM_TOTAL_COUNT" + totalPrice.value = "" + originPrice.value = "" + meetingAddress.value = "" + meetingAddressDetail.value = "" + meetingDate.value = "" + meetingDateValue.value = "" + description.value = "" + } - companion object { - private const val ERROR_INTEGER_FORMAT = -1 - private const val ERROR_FLOAT_FORMAT = -1f - private const val MINIMUM_TOTAL_COUNT = 2 - private const val MAXIMUM_TOTAL_COUNT = 10_000 - private const val INPUT_DATE_TIME_FORMAT = "yyyy년 M월 d일 a h시 m분" - private const val INPUT_DATE_FORMAT = "yyyy년 M월 d일" - private const val OUTPUT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" - const val HTTPS = "https:" - - @Suppress("UNCHECKED_CAST") - fun getFactory( - offeringRepository: OfferingRepository, - authRepository: AuthRepository, - ) = object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - return OfferingWriteViewModel( - offeringRepository, - authRepository, - ) as T - } + companion object { + private const val ERROR_INTEGER_FORMAT = -1 + private const val ERROR_FLOAT_FORMAT = -1f + private const val MINIMUM_TOTAL_COUNT = 2 + private const val MAXIMUM_TOTAL_COUNT = 10_000 + private const val INPUT_DATE_TIME_FORMAT = "yyyy년 M월 d일 a h시 m분" + private const val INPUT_DATE_FORMAT = "yyyy년 M월 d일" + private const val OUTPUT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" + const val HTTPS = "https:" } } -} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OnOfferingWriteClickListener.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OnDateTimeButtonsClickListener.kt similarity index 75% rename from android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OnOfferingWriteClickListener.kt rename to android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OnDateTimeButtonsClickListener.kt index 56fe0e55b..477df6f5f 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OnOfferingWriteClickListener.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/write/OnDateTimeButtonsClickListener.kt @@ -1,6 +1,6 @@ package com.zzang.chongdae.presentation.view.write -interface OnOfferingWriteClickListener { +interface OnDateTimeButtonsClickListener { fun onDateTimeSubmitButtonClick() fun onDateTimeCancelButtonClick() diff --git a/android/app/src/main/res/layout/activity_comment_detail.xml b/android/app/src/main/res/layout/activity_comment_detail.xml index 7cc1621fd..f39ec8188 100644 --- a/android/app/src/main/res/layout/activity_comment_detail.xml +++ b/android/app/src/main/res/layout/activity_comment_detail.xml @@ -38,7 +38,7 @@ android:layout_marginStart="@dimen/margin_10" android:layout_marginTop="@dimen/margin_20" android:contentDescription="@string/comment_detail" - android:onClick="@{() -> vm.onBackClick()}" + app:debouncedOnClick="@{() -> vm.onBackClick()}" android:padding="@dimen/margin_10" android:src="@drawable/btn_left_vector" app:layout_constraintStart_toStartOf="parent" @@ -112,7 +112,7 @@ android:elevation="5dp" android:fontFamily="@font/suit_semibold" android:gravity="center" - android:onClick="@{() -> vm.updateOfferingEvent()}" + app:debouncedOnClick="@{() -> vm.updateOfferingEvent()}" android:text="@{vm.commentOfferingInfo.buttonText}" android:textColor="@color/white" android:textSize="@dimen/size_15" @@ -128,7 +128,7 @@ android:layout_width="match_parent" android:layout_height="@dimen/size_36" android:background="@color/gray_100" - android:onClick="@{() -> vm.toggleCollapsibleView()}" + app:debouncedOnClick="@{() -> vm.toggleCollapsibleView()}" android:translationZ="1dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -259,7 +259,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_20" - android:layout_marginEnd="@dimen/margin_20" + android:layout_marginEnd="@dimen/size_23" android:layout_marginBottom="@dimen/margin_30" android:background="@drawable/bg_gray100_radius_16dp" android:fontFamily="@font/suit_medium" @@ -279,9 +279,9 @@ @@ -391,7 +391,7 @@ android:layout_width="@dimen/icon_size_24" android:layout_height="@dimen/icon_size_24" android:layout_marginStart="@dimen/margin_20" - android:onClick="@{() -> vm.exitOffering()}" + app:debouncedOnClick="@{() -> vm.onExitClick()}" android:src="@drawable/btn_exit" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/android/app/src/main/res/layout/activity_login.xml b/android/app/src/main/res/layout/activity_login.xml index 2023b3e1c..eb23270e4 100644 --- a/android/app/src/main/res/layout/activity_login.xml +++ b/android/app/src/main/res/layout/activity_login.xml @@ -55,7 +55,7 @@ android:layout_marginEnd="@dimen/margin_30" android:layout_marginBottom="@dimen/size_120" android:background="@drawable/bg_yellow_radius_12dp" - android:onClick="@{() -> onAuthClickListener.onLoginButtonClick()}" + app:debouncedOnClick="@{() -> onAuthClickListener.onLoginButtonClick()}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/android/app/src/main/res/layout/dialog_alert.xml b/android/app/src/main/res/layout/dialog_alert.xml new file mode 100644 index 000000000..aaa16919c --- /dev/null +++ b/android/app/src/main/res/layout/dialog_alert.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/dialog_date_picker.xml b/android/app/src/main/res/layout/dialog_date_picker.xml index 78d4e866c..cadfeb816 100644 --- a/android/app/src/main/res/layout/dialog_date_picker.xml +++ b/android/app/src/main/res/layout/dialog_date_picker.xml @@ -2,14 +2,9 @@ - - - + type="com.zzang.chongdae.presentation.view.write.OnDateTimeButtonsClickListener" /> - - + app:layout_constraintTop_toTopOf="parent" />--> + app:layout_constraintTop_toTopOf="parent" /> - + tools:layout_editor_absoluteX="10dp" />--> +
+ +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java index 6f8cc7ddf..255aff60b 100644 --- a/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java +++ b/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.restassured.http.ContentType; import java.time.Duration; +import java.util.Base64; import java.util.Date; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -88,9 +89,9 @@ void should_loginSuccess_when_givenMemberCI() { } } - @DisplayName("토큰 재발급") + @DisplayName("토큰 관리") @Nested - class Refresh { + class ManageToken { List responseHeaderDescriptors = List.of( headerWithName("Set-Cookie").description(""" @@ -111,9 +112,15 @@ class Refresh { @Value("${security.jwt.token.refresh-secret-key}") String refreshSecretKey; + @Value("${security.jwt.token.access-secret-key}") + String accessSecretKey; + @Value("${security.jwt.token.refresh-token-expired}") Duration refreshTokenExpired; + @Value("${security.jwt.token.access-token-expired}") + Duration accessTokenExpired; + MemberEntity member; Date now; @@ -123,6 +130,35 @@ void setUp() { now = Date.from(clock.instant()); } + @DisplayName("만료된 accessToken 경우 예외 발생 후 401 코드를 반환한다.") + @Test + void should_throwException_when_givenExpiredAccessToken() { + Date alreadyExpiredAt = new Date(now.getTime() - accessTokenExpired.toMillis()); + String expiredToken = Jwts.builder() + .setSubject(member.getId().toString()) + .setExpiration(alreadyExpiredAt) + .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(accessSecretKey.getBytes())) + .compact(); + + given(spec).log().all() + .filter(document("access-fail-expired-token", resource(failedSnippets))) + .cookie("access_token", expiredToken) + .when().get("/offerings") + .then().log().all() + .statusCode(401); + } + + @DisplayName("유효하지 않은 accessToken인 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidAccessToken() { + given(spec).log().all() + .filter(document("refresh-fail-invalid-token", resource(failedSnippets))) + .cookie("access_token", "invalidRefreshToken") + .when().post("/offerings") + .then().log().all() + .statusCode(401); + } + @DisplayName("refreshToken으로 accessToken과 refreshToken을 재발급 한다.") @Test void should_refreshSuccess_when_givenRefreshToken() { @@ -147,14 +183,14 @@ void should_throwException_when_givenInvalidRefreshToken() { .statusCode(401); } - @DisplayName("만료된 refeshToken인 경우 예외가 발생한다.") + @DisplayName("만료된 refeshToken인 경우 예외 발생 후 403 코드를 반환한다.") @Test void should_throwException_when_givenExpiredRefreshToken() { Date alreadyExpiredAt = new Date(now.getTime() - refreshTokenExpired.toMillis()); String expiredToken = Jwts.builder() .setSubject(member.getId().toString()) .setExpiration(alreadyExpiredAt) - .signWith(SignatureAlgorithm.HS256, refreshSecretKey) + .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(refreshSecretKey.getBytes())) .compact(); given(spec).log().all() @@ -162,7 +198,7 @@ void should_throwException_when_givenExpiredRefreshToken() { .cookie("refresh_token", expiredToken) .when().post("/auth/refresh") .then().log().all() - .statusCode(401); + .statusCode(403); } } } diff --git a/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java index 16171d61e..fc2ed916a 100644 --- a/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java +++ b/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java @@ -225,9 +225,9 @@ void should_responseCommentRoomInfo_when_givenOfferingId() { .statusCode(200); } - @DisplayName("유효하지 않은 공모에 대해 댓글방 정보를 조회할 경우 예외가 발생한다.") + @DisplayName("존재하지 않는 공모 id로 댓글방 정보를 조회할 경우 예외가 발생한다.") @Test - void should_throwException_when_invalidOffering() { + void should_throwException_when_givenInvalidOffering() { given(spec).log().all() .filter(document("get-comment-room-info-fail-invalid-offering", resource(failSnippets))) .cookies(cookieProvider.createCookiesWithMember(member)) @@ -239,7 +239,7 @@ void should_throwException_when_invalidOffering() { @DisplayName("총대 혹은 참여자가 아닌 사용자가 댓글방 정보를 조회할 경우 예외가 발생한다.") @Test - void should_throwException_when_invalidMember() { + void should_throwException_when_givenInvalidMember() { given(spec).log().all() .filter(document("get-comment-room-info-fail-invalid-member", resource(failSnippets))) .cookies(cookieProvider.createCookiesWithMember(invalidMember)) diff --git a/backend/src/test/java/com/zzang/chongdae/comment/service/CommentServiceTest.java b/backend/src/test/java/com/zzang/chongdae/comment/service/CommentServiceTest.java new file mode 100644 index 000000000..d582fb2d5 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/comment/service/CommentServiceTest.java @@ -0,0 +1,98 @@ +package com.zzang.chongdae.comment.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.zzang.chongdae.comment.service.dto.CommentRoomAllResponse; +import com.zzang.chongdae.comment.service.dto.CommentRoomInfoResponse; +import com.zzang.chongdae.global.service.ServiceTest; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.domain.CommentRoomStatus; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class CommentServiceTest extends ServiceTest { + + @Autowired + CommentService commentService; + + @DisplayName("댓글방 목록 조회") + @Nested + class GetAllCommentRoom { + + MemberEntity member; + OfferingEntity offering; + + @BeforeEach + void setUp() { + member = memberFixture.createMember("dora"); + offering = offeringFixture.createOffering(member); + offeringMemberFixture.createProposer(member, offering); + } + + @DisplayName("로그인한 유저가 참여한 댓글방 목록을 조회할 수 있다") + @Test + void should_getAllCommentRoom_when_givenLoginMember() { + // when + CommentRoomAllResponse response = commentService.getAllCommentRoom(member); + + // then + assertEquals(response.offerings().size(), 1); + } + + @DisplayName("댓글방 목록 조회 시 삭제된 공모에 대한 댓글방은 제목에 삭제되었다고 명시되어 있다") + @Test + void should_getAllCommentRoomWithDeletedCommentRoom_when_giveLoginMember() { + // given + offeringFixture.deleteOffering(offering); + + // when + CommentRoomAllResponse response = commentService.getAllCommentRoom(member); + + // then + assertEquals(response.offerings().get(0).offeringTitle(), "삭제된 공동구매입니다."); + } + } + + @DisplayName("댓글방 정보 조회") + @Nested + class GetCommentRoomInfo { + + MemberEntity member; + OfferingEntity offering; + + @BeforeEach + void setUp() { + member = memberFixture.createMember("dora"); + offering = offeringFixture.createOffering(member); + offeringMemberFixture.createProposer(member, offering); + } + + @DisplayName("삭제되지 않은 공모 id를 통해 댓글방 상세 조회를 할 수 있다") + @Test + void should_getExistedCommentRoomInfo_when_givenOfferingId() { + // when + CommentRoomInfoResponse response = commentService.getCommentRoomInfo(offering.getId(), member); + + // then + assertEquals(response.title(), offering.getTitle()); + } + + @DisplayName("삭제된 공모 id를 통해 삭제된 공모라고 명시된 댓글방 상세 조회를 할 수 있다") + @Test + void should_getDeletedCommentRoomInfo_when_givenDeletedOfferingId() { + // given + offeringFixture.deleteOffering(offering); + + // when + CommentRoomInfoResponse response = commentService.getCommentRoomInfo(offering.getId(), member); + + // then + assertEquals(response.status(), CommentRoomStatus.DELETED); + assertEquals(response.title(), "삭제된 공동구매입니다."); + } + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java b/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java index 432193cff..cfa82bb56 100644 --- a/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java @@ -3,6 +3,9 @@ import com.zzang.chongdae.member.domain.AuthProvider; import com.zzang.chongdae.member.repository.MemberRepository; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -20,4 +23,13 @@ public MemberEntity createMember(String nickname) { "1234"); return memberRepository.save(member); } + + public List createMembers(int memberCount) { + List members = new ArrayList<>(); + for (int i = 0; i < memberCount; i++) { + MemberEntity member = createMember("user_%d".formatted(i)); + members.add(member); + } + return Collections.unmodifiableList(members); + } } diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java b/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java index 952233d36..e59872c5b 100644 --- a/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java @@ -15,10 +15,14 @@ public class OfferingFixture { @Autowired private OfferingRepository offeringRepository; - public OfferingEntity createOffering(MemberEntity member, CommentRoomStatus commentRoomStatus) { + private OfferingEntity createOffering(MemberEntity member, + String title, + Double discountRate, + OfferingStatus offeringStatus, + CommentRoomStatus commentRoomStatus) { OfferingEntity offering = new OfferingEntity( member, - "title", + title, "description", "thumbnailUrl", "productUrl", @@ -30,16 +34,42 @@ public OfferingEntity createOffering(MemberEntity member, CommentRoomStatus comm 1, 5000, 1000, - 33.3, - OfferingStatus.AVAILABLE, // TODO : 데이터 정합성 맞추기 + discountRate, + offeringStatus, commentRoomStatus ); return offeringRepository.save(offering); } + public OfferingEntity createOffering(MemberEntity member, Double discountRate) { + return createOffering(member, "title", discountRate, OfferingStatus.AVAILABLE, CommentRoomStatus.GROUPING); + } + + public OfferingEntity createOffering(MemberEntity member, CommentRoomStatus commentRoomStatus) { + return createOffering(member, "title", 33.3, OfferingStatus.AVAILABLE, commentRoomStatus); + } + + public OfferingEntity createOffering(MemberEntity member, OfferingStatus offeringStatus) { + return createOffering(member, "title", 33.3, offeringStatus, CommentRoomStatus.GROUPING); + } + public OfferingEntity createOffering(MemberEntity member) { - return createOffering(member, CommentRoomStatus.GROUPING); + return createOffering(member, "title", 33.3, OfferingStatus.AVAILABLE, CommentRoomStatus.GROUPING); + } + + public OfferingEntity createOffering(MemberEntity member, String title) { + return createOffering(member, title, 33.3, OfferingStatus.AVAILABLE, CommentRoomStatus.GROUPING); } + public void deleteOffering(OfferingEntity offering) { + offeringRepository.delete(offering); + } + public void deleteOfferingById(Long offeringId) { + offeringRepository.deleteById(offeringId); + } + + public long countOffering() { + return offeringRepository.count(); + } } diff --git a/backend/src/test/java/com/zzang/chongdae/member/service/NicknameGeneratorTest.java b/backend/src/test/java/com/zzang/chongdae/member/service/NicknameGeneratorTest.java index ef75eb20b..42e9f7a23 100644 --- a/backend/src/test/java/com/zzang/chongdae/member/service/NicknameGeneratorTest.java +++ b/backend/src/test/java/com/zzang/chongdae/member/service/NicknameGeneratorTest.java @@ -18,7 +18,7 @@ public class NicknameGeneratorTest { @Test void should_returnNickname_when_generateNickName() { // given - String expected = "춤추는도라"; + String expected = "춤추는장성"; // when String actual = nickNameGenerator.generate(); diff --git a/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java index 1db7b21a8..d660abd9f 100644 --- a/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java +++ b/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java @@ -13,6 +13,7 @@ import com.epages.restdocs.apispec.ResourceSnippetParameters; import com.zzang.chongdae.global.integration.IntegrationTest; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.domain.CommentRoomStatus; import com.zzang.chongdae.offering.domain.OfferingFilter; import com.zzang.chongdae.offering.domain.OfferingFilterType; import com.zzang.chongdae.offering.domain.OfferingStatus; @@ -20,6 +21,7 @@ import com.zzang.chongdae.offering.service.dto.OfferingMeetingUpdateRequest; import com.zzang.chongdae.offering.service.dto.OfferingProductImageRequest; import com.zzang.chongdae.offering.service.dto.OfferingSaveRequest; +import com.zzang.chongdae.offering.service.dto.OfferingUpdateRequest; import com.zzang.chongdae.storage.service.StorageService; import io.restassured.http.ContentType; import java.io.File; @@ -48,24 +50,25 @@ class GetOfferingDetail { parameterWithName("offering-id").description("공모 id (필수)") ); List successResponseDescriptors = List.of( - fieldWithPath("id").description("공모 id"), - fieldWithPath("title").description("제목"), + fieldWithPath("id").description("공모 id (필수)"), + fieldWithPath("title").description("제목 (필수)"), fieldWithPath("productUrl").description("물품 링크"), - fieldWithPath("meetingAddress").description("모집 주소"), + fieldWithPath("meetingAddress").description("모집 주소 (필수)"), fieldWithPath("meetingAddressDetail").description("모집 상세 주소"), - fieldWithPath("description").description("내용"), - fieldWithPath("meetingDate").description("마감시간"), - fieldWithPath("currentCount").description("현재원"), - fieldWithPath("totalCount").description("총원"), + fieldWithPath("description").description("내용 (필수)"), + fieldWithPath("meetingDate").description("마감시간 (필수)"), + fieldWithPath("currentCount").description("현재원 (필수)"), + fieldWithPath("totalCount").description("총원 (필수)"), fieldWithPath("thumbnailUrl").description("사진 링크"), - fieldWithPath("dividedPrice").description("n빵 가격"), - fieldWithPath("totalPrice").description("총가격"), - fieldWithPath("status").description("공모 상태" + fieldWithPath("dividedPrice").description("n빵 가격 (필수)"), + fieldWithPath("totalPrice").description("총가격 (필수)"), + fieldWithPath("originPrice").description("원 가격"), + fieldWithPath("status").description("공모 상태 (필수)" + getEnumValuesAsString(OfferingStatus.class)), - fieldWithPath("memberId").description("공모자 회원 id"), - fieldWithPath("nickname").description("공모자 회원 닉네임"), - fieldWithPath("isProposer").description("공모자 여부"), - fieldWithPath("isParticipated").description("공모 참여 여부") + fieldWithPath("memberId").description("공모자 회원 id (필수)"), + fieldWithPath("nickname").description("공모자 회원 닉네임 (필수)"), + fieldWithPath("isProposer").description("공모자 여부 (필수)"), + fieldWithPath("isParticipated").description("공모 참여 여부 (필수)") ); ResourceSnippetParameters successSnippets = builder() .summary("공모 상세 조회") @@ -510,7 +513,7 @@ void should_createOffering_when_givenOfferingCreateRequest() { "서울특별시 광진구 구의강변로 3길 11", "상세주소아파트", "구의동", - LocalDateTime.parse("2024-10-11T10:00:00"), + LocalDateTime.now().plusDays(1), "내용입니다." ); @@ -537,7 +540,7 @@ void should_createOffering_when_givenOfferingWithoutOriginPriceCreateRequest() { "서울특별시 광진구 구의강변로 3길 11", "상세주소아파트", "구의동", - LocalDateTime.parse("2024-10-11T10:00:00"), + LocalDateTime.now().plusDays(1), "내용입니다." ); @@ -631,6 +634,33 @@ void should_throwException_when_overMaximumTotalCount() { .then().log().all() .statusCode(400); } + + @DisplayName("거래 날짜를 내일보다 과거로 설정하는 경우 예외가 발생한다.") + @Test + void should_throwException_when_meetingDateBeforeTomorrow() { + OfferingSaveRequest request = new OfferingSaveRequest( + "공모 제목", + "www.naver.com", + "www.naver.com/favicon.ico", + 10, + 10000, + 2000, + "서울특별시 광진구 구의강변로 3길 11", + "상세주소아파트", + "구의동", + LocalDateTime.now(), + "내용입니다." + ); + + given(spec).log().all() + .filter(document("create-offering-fail-with-invalid-meeting-date", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(member)) + .contentType(ContentType.JSON) + .body(request) + .when().post("/offerings") + .then().log().all() + .statusCode(400); + } } @DisplayName("상품 이미지 추출") @@ -724,7 +754,6 @@ class UploadProductImage { .summary("상품 이미지 업로드") .description(""" 상품 이미지를 받아 이미지를 S3에 업로드한다. - 현재 사용 플러그인이 multipart/form-data의 파라미터에 대한 문서화를 지원하지 않습니다. ### Parameters | Part | Type | Description | @@ -760,4 +789,329 @@ void should_uploadImageUrl_when_givenImageFile() { .statusCode(200); } } + + @DisplayName("공모 수정") + @Nested + class UpdateOffering { + + List pathParameterDescriptors = List.of( + parameterWithName("offering-id").description("공모 id (필수)") + ); + + List requestDescriptors = List.of( + fieldWithPath("title").description("제목 (필수)"), + fieldWithPath("productUrl").description("물품 구매 링크"), + fieldWithPath("thumbnailUrl").description("사진 링크"), + fieldWithPath("totalCount").description("총원 (필수)"), + fieldWithPath("totalPrice").description("총가격 (필수)"), + fieldWithPath("originPrice").description("원 가격"), + fieldWithPath("meetingAddress").description("모집 주소 (필수)"), + fieldWithPath("meetingAddressDetail").description("모집 상세 주소"), + fieldWithPath("meetingAddressDong").description("모집 동 주소"), + fieldWithPath("meetingDate").description("모집 종료 시간 (필수)"), + fieldWithPath("description").description("내용 (필수)") + ); + + List successResponseDescriptors = List.of( + fieldWithPath("id").description("공모 id"), + fieldWithPath("title").description("제목"), + fieldWithPath("productUrl").description("물품 링크"), + fieldWithPath("meetingAddress").description("모집 주소"), + fieldWithPath("meetingAddressDetail").description("모집 상세 주소"), + fieldWithPath("description").description("내용"), + fieldWithPath("meetingDate").description("마감시간"), + fieldWithPath("currentCount").description("현재원"), + fieldWithPath("totalCount").description("총원"), + fieldWithPath("thumbnailUrl").description("사진 링크"), + fieldWithPath("dividedPrice").description("n빵 가격"), + fieldWithPath("totalPrice").description("총가격"), + fieldWithPath("status").description("공모 상태" + + getEnumValuesAsString(OfferingStatus.class)), + fieldWithPath("memberId").description("공모자 회원 id"), + fieldWithPath("nickname").description("공모자 회원 닉네임") + ); + + ResourceSnippetParameters successSnippets = builder() + .summary("공모 수정") + .description("공모 정보를 받아 공모를 수정합니다.") + .pathParameters(pathParameterDescriptors) + .requestFields(requestDescriptors) + .responseFields(successResponseDescriptors) + .requestSchema(schema("OfferingUpdateRequest")) + .build(); + + ResourceSnippetParameters failSnippets = builder() + .summary("공모 수정") + .description("공모 정보를 받아 공모를 수정합니다.") + .requestFields(requestDescriptors) + .responseFields(failResponseDescriptors) + .requestSchema(schema("OfferingUpdateRequest")) + .responseSchema(schema("OfferingUpdateFailResponse")) + .build(); + + MemberEntity proposer; + MemberEntity otherMember; + + @BeforeEach + void setUp() { + proposer = memberFixture.createMember("poke"); + otherMember = memberFixture.createMember("other"); + OfferingEntity offering = offeringFixture.createOffering(proposer); + offeringMemberFixture.createProposer(proposer, offering); + List participants = memberFixture.createMembers(9); + participants.forEach(participant -> offeringMemberFixture.createParticipant(participant, offering)); + } + + @DisplayName("공모를 수정할 수 있다.") + @Test + void should_updateOffering_when_givenOfferingId() { + OfferingUpdateRequest request = new OfferingUpdateRequest( + "수정할 제목", + "https://to.be.updated/productUrl", + "https://to.be.updated/thumbnail/url", + 20, + 20000, + 5000, + "수정할 모집 장소 주소", + "수정할 모집 상세 주소", + "수정된동", + LocalDateTime.parse("2024-10-25T00:00:00"), + "수정할 공모 상세 내용" + ); + + given(spec).log().all() + .filter(document("update-offering-success", resource(successSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .contentType(ContentType.JSON) + .pathParam("offering-id", 1) + .body(request) + .when().patch("/offerings/{offering-id}") + .then().log().all() + .statusCode(200); + } + + @DisplayName("제안자가 아닌 사용자가 공모를 수정할 수 없다.") + @Test + void should_throwException_when_updateOtherMember() { + OfferingUpdateRequest request = new OfferingUpdateRequest( + "수정할 제목", + "https://to.be.updated/productUrl", + "https://to.be.updated/thumbnail/url", + 20, + 20000, + 5000, + "수정할 모집 장소 주소", + "수정할 모집 상세 주소", + "수정된동", + LocalDateTime.parse("2024-10-25T00:00:00"), + "수정할 공모 상세 내용" + ); + + given(spec).log().all() + .filter(document("update-offering-fail-not-proposer", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(otherMember)) + .contentType(ContentType.JSON) + .pathParam("offering-id", 1) + .body(request) + .when().patch("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("참여 인원 이하 인원으로 공모를 수정할 수 없다.") + @Test + void should_throwException_when_updateTotalCountLessEqualThanCurrentCount() { + OfferingUpdateRequest request = new OfferingUpdateRequest( + "수정할 제목", + "https://to.be.updated/productUrl", + "https://to.be.updated/thumbnail/url", + 9, + 20000, + 5000, + "수정할 모집 장소 주소", + "수정할 모집 상세 주소", + "수정된동", + LocalDateTime.parse("2024-10-25T00:00:00"), + "수정할 공모 상세 내용" + ); + + given(spec).log().all() + .filter(document("update-offering-fail-less-than-current-count", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .contentType(ContentType.JSON) + .pathParam("offering-id", 1) + .body(request) + .when().patch("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("모집 날짜가 현재와 같거나 지날 경우 수정할 수 없다.") + @Test + void should_throwException_when_modifyMeetingDateBeforeNowToday() { + OfferingUpdateRequest request = new OfferingUpdateRequest( + "수정할 제목", + "https://to.be.updated/productUrl", + "https://to.be.updated/thumbnail/url", + 20, + 20000, + 5000, + "수정할 모집 장소 주소", + "수정할 모집 상세 주소", + "수정된동", + LocalDateTime.now(), + "수정할 공모 상세 내용" + ); + + given(spec).log().all() + .filter(document("update-offering-fail-before-now-today", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .contentType(ContentType.JSON) + .pathParam("offering-id", 1) + .body(request) + .when().patch("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("수정 할 원가격이 N빵 가격보다 작을경우 수정할 수 없다.") + @Test + void should_throwException_when_originPriceLessThanDividePrice() { + OfferingUpdateRequest request = new OfferingUpdateRequest( + "수정할 제목", + "https://to.be.updated/productUrl", + "https://to.be.updated/thumbnail/url", + 20, + 20000, + 500, + "수정할 모집 장소 주소", + "수정할 모집 상세 주소", + "수정된동", + LocalDateTime.parse("2024-10-25T00:00:00"), + "수정할 공모 상세 내용" + ); + + given(spec).log().all() + .filter(document("patch-offering-fail-less-than-divide-price", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .contentType(ContentType.JSON) + .pathParam("offering-id", 1) + .body(request) + .when().patch("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + } + + @DisplayName("공모 삭제") + @Nested + class DeleteOffering { + + List pathParameterDescriptors = List.of( + parameterWithName("offering-id").description("공모 id (필수)") + ); + + ResourceSnippetParameters successSnippets = builder() + .summary("공모 삭제") + .description("공모 id를 통해 공모를 삭제합니다.") + .pathParameters(pathParameterDescriptors) + .responseSchema(schema("OfferingDeleteSuccessResponse")) + .build(); + ResourceSnippetParameters failSnippets = builder() + .summary("공모 삭제") + .description("공모 id를 통해 공모를 삭제합니다.") + .pathParameters(pathParameterDescriptors) + .responseFields(failResponseDescriptors) + .responseSchema(schema("OfferingDeleteFailResponse")) + .build(); + + MemberEntity proposer; + MemberEntity notProposer; + MemberEntity participant; + OfferingEntity offering; + OfferingEntity offeringInProgress; + OfferingEntity offeringDone; + + @BeforeEach + void setUp() { + notProposer = memberFixture.createMember("never"); + proposer = memberFixture.createMember("ever"); + offering = offeringFixture.createOffering(proposer); + participant = memberFixture.createMember("naver"); + offeringMemberFixture.createParticipant(participant, offering); + offeringInProgress = offeringFixture.createOffering(proposer, CommentRoomStatus.TRADING); + offeringDone = offeringFixture.createOffering(proposer, CommentRoomStatus.DONE); + } + + @DisplayName("공모 id로 공모를 삭제할 수 있다") + @Test + void should_deleteOffering_when_givenOfferingId() { + given(spec).log().all() + .filter(document("delete-offering-success", resource(successSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .pathParam("offering-id", offering.getId()) + .when().delete("/offerings/{offering-id}") + .then().log().all() + .statusCode(204); + } + + @DisplayName("유효하지 않은 공모 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_invalidOffering() { + given(spec).log().all() + .filter(document("delete-offering-fail-invalid-offering", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .pathParam("offering-id", offering.getId() + 9999) + .when().delete("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("총대가 아닌 사용자가 공모 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_notProposer() { + given(spec).log().all() + .filter(document("delete-offering-fail-not-proposer", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(notProposer)) + .pathParam("offering-id", offering.getId()) + .when().delete("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("참여자가 공모 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_participant() { + given(spec).log().all() + .filter(document("delete-offering-fail-participant", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(participant)) + .pathParam("offering-id", offering.getId()) + .when().delete("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("거래 진행 중 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_unavailableStatus() { + given(spec).log().all() + .filter(document("delete-offering-fail-in-progress", resource(failSnippets))) + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .pathParam("offering-id", offeringInProgress.getId()) + .when().delete("/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + + @DisplayName("거래가 완료된 공모를 삭제할 수 있다.") + @Test + void should_deleteOffering_when_givenDoneOffering() { + given().log().all() + .cookies(cookieProvider.createCookiesWithMember(proposer)) + .pathParam("offering-id", offeringDone.getId()) + .when().delete("/offerings/{offering-id}") + .then().log().all() + .statusCode(204); + } + } } diff --git a/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingReadOnlyIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingReadOnlyIntegrationTest.java new file mode 100644 index 000000000..9a16a4f66 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingReadOnlyIntegrationTest.java @@ -0,0 +1,186 @@ +package com.zzang.chongdae.offering.integration; + +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder; +import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.Schema.schema; +import static io.restassured.RestAssured.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +import com.epages.restdocs.apispec.ParameterDescriptorWithType; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.zzang.chongdae.global.integration.IntegrationTest; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.domain.OfferingFilter; +import com.zzang.chongdae.offering.domain.OfferingFilterType; +import com.zzang.chongdae.offering.domain.OfferingStatus; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.payload.FieldDescriptor; + +public class OfferingReadOnlyIntegrationTest extends IntegrationTest { + + @DisplayName("공모 상세 조회(읽기 전용)") + @Nested + class ReadOnlyGetOffering { + List pathParameterDescriptors = List.of( + parameterWithName("offering-id").description("공모 id (필수)") + ); + List successResponseDescriptors = List.of( + fieldWithPath("id").description("공모 id"), + fieldWithPath("title").description("제목"), + fieldWithPath("meetingAddressDong").description("모집 동 주소"), + fieldWithPath("currentCount").description("현재원"), + fieldWithPath("totalCount").description("총원"), + fieldWithPath("thumbnailUrl").description("사진 링크"), + fieldWithPath("dividedPrice").description("n빵 가격"), + fieldWithPath("originPrice").description("원 가격"), + fieldWithPath("discountRate").description("할인율"), + fieldWithPath("status").description("공모 상태" + + getEnumValuesAsString(OfferingStatus.class)), + fieldWithPath("isOpen").description("공모 참여 가능 여부") + ); + ResourceSnippetParameters successSnippets = builder() + .summary("공모 단건 조회") + .description("공모 단건을 조회합니다.") + .pathParameters(pathParameterDescriptors) + .responseFields(successResponseDescriptors) + .responseSchema(schema("OfferingReadOnlySuccessResponse")) + .build(); + ResourceSnippetParameters failSnippets = builder() + .summary("공모 단건 조회") + .description("공모 id를 통해 공모의 단건 정보를 조회합니다.") + .pathParameters(pathParameterDescriptors) + .responseFields(failResponseDescriptors) + .responseSchema(schema("OfferingReadOnlyFailResponse")) + .build(); + + @BeforeEach + void setUp() { + MemberEntity proposer = memberFixture.createMember("dora"); + OfferingEntity offering = offeringFixture.createOffering(proposer); + offeringMemberFixture.createProposer(proposer, offering); + } + + @DisplayName("공모 단건을 조회할 수 있다") + @Test + void should_responseOffering_when_givenOfferingId() { + given(spec).log().all() + .filter(document("get-offering-read-only-success", resource(successSnippets))) + .pathParam("offering-id", 1) + .when().get("/read-only/offerings/{offering-id}") + .then().log().all() + .statusCode(200); + } + + @DisplayName("유효하지 않은 공모를 단건 조회할 경우 예외가 발생한다.") + @Test + void should_throwException_when_invalidOffering() { + given(spec).log().all() + .filter(document("get-offering-read-only-fail-invalid-offering", resource(failSnippets))) + .pathParam("offering-id", 100) + .when().get("/read-only/offerings/{offering-id}") + .then().log().all() + .statusCode(400); + } + } + + @DisplayName("공모 목록 조회(읽기 전용)") + @Nested + class ReadOnlyGetAllOffering { + + List queryParameterDescriptors = List.of( + parameterWithName("filter").description("필터 이름 (기본값: RECENT)" + + getEnumValuesAsString(OfferingFilter.class)).optional(), + parameterWithName("search").description("검색어").optional(), + parameterWithName("last-id").description("마지막 공모 id").optional(), + parameterWithName("page-size").description("페이지 크기 (기본값: 10)").optional() + ); + List successResponseDescriptors = List.of( + fieldWithPath("offerings[].id").description("공모 id"), + fieldWithPath("offerings[].title").description("제목"), + fieldWithPath("offerings[].meetingAddressDong").description("모집 동 주소"), + fieldWithPath("offerings[].currentCount").description("현재원"), + fieldWithPath("offerings[].totalCount").description("총원"), + fieldWithPath("offerings[].thumbnailUrl").description("사진 링크"), + fieldWithPath("offerings[].dividedPrice").description("n빵 가격"), + fieldWithPath("offerings[].originPrice").description("원 가격"), + fieldWithPath("offerings[].discountRate").description("할인율"), + fieldWithPath("offerings[].status").description("공모 상태" + + getEnumValuesAsString(OfferingStatus.class)), + fieldWithPath("offerings[].isOpen").description("공모 참여 가능 여부") + ); + ResourceSnippetParameters successSnippets = builder() + .summary("공모 목록 조회") + .description("공모 목록을 조회합니다.") + .queryParameters(queryParameterDescriptors) + .responseFields(successResponseDescriptors) + .responseSchema(schema("OfferingAllReadOnlySuccessResponse")) + .build(); + + + @BeforeEach + void setUp() { + MemberEntity proposer = memberFixture.createMember("dora"); + + for (int i = 0; i < 11; i++) { + OfferingEntity offering = offeringFixture.createOffering(proposer); + offeringMemberFixture.createProposer(proposer, offering); + } + } + + @DisplayName("공모 목록을 조회할 수 있다") + @Test + void should_responseAllOffering_when_givenPageInfo() { + given(spec).log().all() + .filter(document("get-all-offering-read-only-success", resource(successSnippets))) + .queryParam("filter", "RECENT") + .queryParam("search", "title") + .queryParam("last-id", 10) + .queryParam("page-size", 10) + .when().get("/read-only/offerings") + .then().log().all() + .statusCode(200); + } + } + + @DisplayName("공모 필터 목록 조회(읽기 전용)") + @Nested + class ReadOnlyGetAllOfferingFilter { + + List successResponseDescriptors = List.of( + fieldWithPath("filters[].name").description("필터 이름" + + getEnumValuesAsString(OfferingFilter.class)), + fieldWithPath("filters[].value").description("필터 디스플레이 이름"), + fieldWithPath("filters[].type").description("필터 디스플레이 여부" + + getEnumValuesAsString(OfferingFilterType.class)) + ); + ResourceSnippetParameters successSnippets = builder() + .summary("공모 필터 목록 조회") + .description("공모 목록 조회 시 필터링할 수 있는 키워드 목록을 조회합니다.") + .responseFields(successResponseDescriptors) + .responseSchema(schema("OfferingFilterReadOnlySuccessResponse")) + .build(); + + + @BeforeEach + void setUp() { + memberFixture.createMember("dora"); + } + + @DisplayName("공모 id로 공모 일정 정보를 조회할 수 있다") + @Test + void should_responseOfferingFilter_when_givenOfferingId() { + given(spec).log().all() + .filter(document("get-all-offering-filter-read-only-success", resource(successSnippets))) + .when().get("/read-only/offerings/filters") + .then().log().all() + .statusCode(200); + } + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/offering/service/OfferingServiceTest.java b/backend/src/test/java/com/zzang/chongdae/offering/service/OfferingServiceTest.java index 56046217e..b776e9a34 100644 --- a/backend/src/test/java/com/zzang/chongdae/offering/service/OfferingServiceTest.java +++ b/backend/src/test/java/com/zzang/chongdae/offering/service/OfferingServiceTest.java @@ -1,16 +1,25 @@ package com.zzang.chongdae.offering.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import com.zzang.chongdae.global.exception.MarketException; import com.zzang.chongdae.global.service.ServiceTest; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.domain.CommentRoomStatus; +import com.zzang.chongdae.offering.domain.OfferingStatus; import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import com.zzang.chongdae.offering.service.dto.OfferingAllResponse; import com.zzang.chongdae.offering.service.dto.OfferingAllResponseItem; +import com.zzang.chongdae.offering.service.dto.OfferingDetailResponse; import com.zzang.chongdae.offering.service.dto.OfferingSaveRequest; +import com.zzang.chongdae.offering.service.dto.OfferingUpdateRequest; import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -19,59 +28,445 @@ public class OfferingServiceTest extends ServiceTest { @Autowired OfferingService offeringService; - @DisplayName("공모 id를 통해 공모를 단건 조회할 수 있다.") - @Test - void should_getOffering_when_givenOfferingId() { - // given - MemberEntity member = memberFixture.createMember("ever"); - OfferingEntity offering = offeringFixture.createOffering(member); - OfferingAllResponseItem expected = new OfferingAllResponseItem(offering, offering.toOfferingPrice()); + @DisplayName("공모 상세 조회") + @Nested + class GetOfferingDetail { - // when - OfferingAllResponseItem actual = offeringService.getOffering(offering.getId()); + MemberEntity member; + OfferingEntity offering; - // then - assertEquals(expected, actual); + @BeforeEach + void setUp() { + member = memberFixture.createMember("dora"); + offering = offeringFixture.createOffering(member); + } + + @DisplayName("공모 id를 통해 공모 상세를 조회할 수 있다") + @Test + void should_getOfferingDetail_when_givenOfferingId() { + // when + OfferingDetailResponse response = offeringService.getOfferingDetail(offering.getId(), member); + + // then + assertEquals(offering.getId(), response.id()); + } + + @DisplayName("존재하지 않는 공모 id를 통해 공모 상세를 조회할 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidOfferingId() { + // given + long invalidOfferingId = offering.getId() + 9999; + + // when & then + assertThatThrownBy(() -> offeringService.getOfferingDetail(invalidOfferingId, member)) + .isInstanceOf(MarketException.class); + } + + @DisplayName("삭제한 공모 id를 통해 공모 상세를 조회할 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenDeletedOfferingId() { + // given + offeringFixture.deleteOffering(offering); + + // when & then + assertThatThrownBy(() -> offeringService.getOfferingDetail(offering.getId(), member)) + .isInstanceOf(MarketException.class); + } + } + + @DisplayName("공모 단건 조회") + @Nested + class GetOffering { + + OfferingEntity offering; + + @BeforeEach + void setUp() { + MemberEntity member = memberFixture.createMember("ever"); + offering = offeringFixture.createOffering(member); + } + + @DisplayName("공모 id를 통해 공모를 단건 조회할 수 있다") + @Test + void should_getOffering_when_givenOfferingId() { + // given + OfferingAllResponseItem expected = new OfferingAllResponseItem(offering, offering.toOfferingPrice()); + + // when + OfferingAllResponseItem actual = offeringService.getOffering(offering.getId()); + + // then + assertEquals(expected, actual); + } + + @DisplayName("존재하지 않는 공모 id를 통해 공모를 단건 조회할 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidOfferingId() { + // when & then + long invalidOfferingId = offering.getId() + 9999; + + assertThatThrownBy(() -> offeringService.getOffering(invalidOfferingId)) + .isInstanceOf(MarketException.class); + } + + @DisplayName("삭제한 공모 id를 통해 공모를 단건 조회할 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenDeletedOfferingId() { + // given + offeringFixture.deleteOffering(offering); + + // when & then + assertThatThrownBy(() -> offeringService.getOffering(offering.getId())) + .isInstanceOf(MarketException.class); + } + } + + @DisplayName("공모 목록 조회") + @Nested + class GetOfferings { + + MemberEntity member; + + @BeforeEach + void setUp() { + member = memberFixture.createMember("dora"); + for (int i = 1; i <= 17; i++) { + offeringFixture.createOffering(member); + } + } + + @DisplayName("최신순으로 공모 목록을 10개씩 조회할 수 있다") + @Test + void should_getRecentOfferings() { + // when + OfferingAllResponse response = offeringService.getAllOffering("RECENT", null, null, 10); + + // then + assertEquals(10, response.offerings().size()); + } + + @DisplayName("마지막 페이지 이후로 최신순으로 공모 목록을 조회할 수 있다") + @Test + void should_getRecentOfferings_when_givenLastId() { + // given + OfferingAllResponse lastResponse = offeringService.getAllOffering("RECENT", null, null, 10); + List offerings = lastResponse.offerings(); + Long lastId = offerings.get(offerings.size() - 1).id(); + + // when + OfferingAllResponse response = offeringService.getAllOffering("RECENT", null, lastId, 10); + + // then + assertEquals(7, response.offerings().size()); + } + + @DisplayName("삭제한 공모는 최신순으로 공모 목록 조회 시 포함되지 않는다") + @Test + void should_notIncludeDeletedOffering_when_getOfferings() { + // given + offeringFixture.deleteOfferingById(1L); + + // when + OfferingAllResponse response = offeringService.getAllOffering("RECENT", null, null, 20); + + // then + assertEquals(16, response.offerings().size()); + } + + @DisplayName("검색어를 지정해 최신순으로 공모 목록을 조회할 수 있다") + @Test + void should_getOfferings_when_givenSearchKeyword() { + // given + offeringFixture.createOffering(member, "검색어"); + + // when + OfferingAllResponse response = offeringService.getAllOffering("RECENT", "검색", null, 10); + + // then + assertEquals(1, response.offerings().size()); + } + + @DisplayName("참여 가능한 공모 목록만 조회할 수 있다") + @Test + void should_getJoinableOfferings() { + // when + OfferingAllResponse response = offeringService.getAllOffering("JOINABLE", null, null, 20); + + // then + assertEquals(17, response.offerings().size()); + } + + @DisplayName("마지막 페이지 이후로 참여 가능한 공모 목록을 조회할 수 있다") + @Test + void should_getJoinableOfferings_when_givenLastId() { + // given + OfferingAllResponse lastResponse = offeringService.getAllOffering("JOINABLE", null, null, 10); + List offerings = lastResponse.offerings(); + Long lastId = offerings.get(offerings.size() - 1).id(); + + // when + OfferingAllResponse response = offeringService.getAllOffering("JOINABLE", null, lastId, 10); + + // then + assertEquals(7, response.offerings().size()); + } + + @DisplayName("삭제한 공모는 참여 가능한 공모 목록 조회 시 포함되지 않는다") + @Test + void should_notIncludeDeletedOffering_when_getJoinableOfferings() { + // given + offeringFixture.deleteOfferingById(1L); + + // when + OfferingAllResponse response = offeringService.getAllOffering("JOINABLE", null, null, 20); + + // then + assertEquals(16, response.offerings().size()); + } + + @DisplayName("마감 임박한 공모 목록만 조회할 수 있다") + @Test + void should_getImminentOfferings() { + // when + offeringFixture.createOffering(member, OfferingStatus.IMMINENT); + OfferingAllResponse response = offeringService.getAllOffering("IMMINENT", null, null, 20); + + // then + assertEquals(1, response.offerings().size()); + } + + @DisplayName("마지막 페이지 이후로 마감 임박한 공모 목록을 조회할 수 있다") + @Test + void should_getImminentOfferings_when_givenLastId() { + // given + offeringFixture.createOffering(member, OfferingStatus.IMMINENT); + offeringFixture.createOffering(member, OfferingStatus.IMMINENT); + OfferingAllResponse lastResponse = offeringService.getAllOffering("IMMINENT", null, null, 1); + List offerings = lastResponse.offerings(); + Long lastId = offerings.get(offerings.size() - 1).id(); + + // when + OfferingAllResponse response = offeringService.getAllOffering("IMMINENT", null, lastId, 1); + + // then + assertEquals(1, response.offerings().size()); + } + + @DisplayName("삭제한 공모는 마감 임박한 공모 목록 조회 시 포함되지 않는다") + @Test + void should_notIncludeDeletedOffering_when_getImminentOfferings() { + // given + OfferingEntity offering = offeringFixture.createOffering(member, OfferingStatus.IMMINENT); + offeringFixture.createOffering(member, OfferingStatus.IMMINENT); + offeringFixture.deleteOffering(offering); + + // when + OfferingAllResponse response = offeringService.getAllOffering("IMMINENT", null, null, 20); + + // then + assertEquals(1, response.offerings().size()); + } + + @DisplayName("높은 할인율 순으로 공모 목록을 조회할 수 있다") + @Test + void should_getHighDiscountOfferings() { + // given + offeringFixture.createOffering(member, 50.0); + offeringFixture.createOffering(member, 40.0); + + // when + OfferingAllResponse response = offeringService.getAllOffering("HIGH_DISCOUNT", null, null, 20); + + // then + assertEquals(50.0, response.offerings().get(0).discountRate()); + assertEquals(40.0, response.offerings().get(1).discountRate()); + assertEquals(33.3, response.offerings().get(2).discountRate()); + } + + @DisplayName("마지막 페이지 이후로 높은 할인율 순으로 공모 목록을 조회할 수 있다") + @Test + void should_getHighDiscountOfferings_when_givenLastDiscountRate() { + // given + offeringFixture.createOffering(member, 50.0); + offeringFixture.createOffering(member, 40.0); + OfferingAllResponse lastResponse = offeringService.getAllOffering("HIGH_DISCOUNT", null, null, 1); + List offerings = lastResponse.offerings(); + Long lastId = offerings.get(offerings.size() - 1).id(); + + // when + OfferingAllResponse response = offeringService.getAllOffering("HIGH_DISCOUNT", null, lastId, 10); + + // then + assertEquals(40.0, response.offerings().get(0).discountRate()); + assertEquals(33.3, response.offerings().get(1).discountRate()); + } + + @DisplayName("삭제한 공모는 높은 할인율 순으로 공모 목록 조회 시 포함되지 않는다") + @Test + void should_notIncludeDeletedOffering_when_getHighDiscountOfferings() { + // given + OfferingEntity offering = offeringFixture.createOffering(member, 50.0); + offeringFixture.createOffering(member, 40.0); + offeringFixture.deleteOffering(offering); + + // when + OfferingAllResponse response = offeringService.getAllOffering("HIGH_DISCOUNT", null, null, 20); + + // then + assertEquals(40.0, response.offerings().get(0).discountRate()); + } + } + + @DisplayName("공모 작성") + @Nested + class CreateOffering { + + @DisplayName("공목 등록 시 원 가격 정보가 없더라도 공모 작성에 성공할 수 있다.") + @Test + void should_createOffering_when_givenOfferingWithoutOriginPriceCreateRequest() { + // given + MemberEntity member = memberFixture.createMember("pizza"); + OfferingSaveRequest request = new OfferingSaveRequest( + "공모 제목", + "www.naver.com", + "www.naver.com/favicon.ico", + 5, + 10000, + null, + "서울특별시 광진구 구의강변로 3길 11", + "상세주소아파트", + "구의동", + LocalDateTime.now().plusDays(1), + "내용입니다." + ); + Long expected = 1L; + + // when + Long actual = offeringService.saveOffering(request, member); + + // then + assertEquals(expected, actual); + } } - @DisplayName("유효하지 않은 공모 id를 통해 공모를 단건 조회할 경우 예외가 발생한다.") - @Test - void should_throwException_when_givenInvalidOfferingId() { - // given - MemberEntity member = memberFixture.createMember("ever"); - OfferingEntity offering = offeringFixture.createOffering(member); + @DisplayName("공모 수정") + @Nested + class UpdateOffering { + + @DisplayName("공모를 수정할 수 있음") + @Test + void should_updateOffering_when_givenOfferingIdAndOfferingUpdateRequest() { + // given + MemberEntity member = memberFixture.createMember("poke"); + OfferingEntity offering = offeringFixture.createOffering(member); + String expected = "수정된 공모 제목"; + OfferingUpdateRequest request = new OfferingUpdateRequest( + expected, + "https://to.be.updated/productUrl", + "https://to.be.updated/thumbnail/url", + 10, + 20000, + 5000, + "수정할 모집 장소 주소", + "수정할 모집 상세 주소", + "수정된동", + LocalDateTime.parse("2024-12-31T00:00:00"), + "수정할 공모 상세 내용" + ); - // when & then - long invalidOfferingId = offering.getId() + 9999; + // when + offeringService.updateOffering(offering.getId(), request, member); + OfferingAllResponseItem modifiedOffering = offeringService.getOffering(offering.getId()); + String actual = modifiedOffering.title(); - assertThatThrownBy(() -> offeringService.getOffering(invalidOfferingId)) - .isInstanceOf(MarketException.class); + // then + assertEquals(expected, actual); + } } - @DisplayName("공목 등록 시 원 가격 정보가 없더라도 공모 작성에 성공할 수 있다.") - @Test - void should_createOffering_when_givenOfferingWithoutOriginPriceCreateRequest() { - // given - MemberEntity member = memberFixture.createMember("pizza"); - OfferingSaveRequest request = new OfferingSaveRequest( - "공모 제목", - "www.naver.com", - "www.naver.com/favicon.ico", - 5, - 10000, - null, - "서울특별시 광진구 구의강변로 3길 11", - "상세주소아파트", - "구의동", - LocalDateTime.parse("2024-10-11T10:00:00"), - "내용입니다." - ); - Long expected = 1L; - - // when - Long actual = offeringService.saveOffering(request, member); - - // then - assertEquals(expected, actual); + @DisplayName("공모 삭제") + @Nested + class DeleteOffering { + + MemberEntity notProposer; + MemberEntity proposer; + + @BeforeEach + void setUp() { + notProposer = memberFixture.createMember("never"); + proposer = memberFixture.createMember("ever"); + } + + @DisplayName("공모 id와 총대 엔티티가 주어졌을 때 공모를 삭제할 수 있다.") + @Test + void should_deleteOfferingSoftly_when_givenOfferingIdAndMember() { + // given + OfferingEntity offering = offeringFixture.createOffering(proposer); + + // when + offeringService.deleteOffering(offering.getId(), proposer); + + // then + assertThat(offeringFixture.countOffering()).isEqualTo(0); + } + + @DisplayName("총대가 아닌 사용자가 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_deleteWithNotProposer() { + // given + OfferingEntity offering = offeringFixture.createOffering(proposer); + + // when & then + assertThatThrownBy(() -> offeringService.deleteOffering(offering.getId(), notProposer)) + .isInstanceOf(MarketException.class); + } + + @DisplayName("유효하지 않은 공모 id에 대해 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_deleteWithInvalidOfferingId() { + // given + OfferingEntity offering = offeringFixture.createOffering(proposer); + + // when & then + long invalidOfferingId = offering.getId() + 9999; + + assertThatThrownBy(() -> offeringService.deleteOffering(invalidOfferingId, proposer)) + .isInstanceOf(MarketException.class); + } + + @DisplayName("거래 인원이 확정되고 거래가 완료되기 전 (구매 중 상태) 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_deleteAtBuyingStatus() { + // given + OfferingEntity offering = offeringFixture.createOffering(proposer, CommentRoomStatus.BUYING); + + // when & then + assertThatThrownBy(() -> offeringService.deleteOffering(offering.getId(), proposer)) + .isInstanceOf(MarketException.class); + } + + @DisplayName("거래 인원이 확정되고 거래가 완료되기 전 (거래 중 상태) 삭제를 시도할 경우 예외가 발생한다.") + @Test + void should_throwException_when_deleteAtTradingStatus() { + // given + OfferingEntity offering = offeringFixture.createOffering(proposer, CommentRoomStatus.TRADING); + + // when & then + assertThatThrownBy(() -> offeringService.deleteOffering(offering.getId(), proposer)) + .isInstanceOf(MarketException.class); + } + + @DisplayName("거래 완료 상태 공모의 경우 삭제가 가능하다.") + @Test + void should_deleteAvailable_when_statusDone() { + // given + OfferingEntity offering = offeringFixture.createOffering(proposer, CommentRoomStatus.DONE); + + // when + offeringService.deleteOffering(offering.getId(), proposer); + + // then + assertThat(offeringFixture.countOffering()).isEqualTo(0); + } } } diff --git a/backend/src/test/resources/static/nickname/adjectives.txt b/backend/src/test/resources/static/nickname/adjectives.txt index be7c8da49..d9dc64607 100644 --- a/backend/src/test/resources/static/nickname/adjectives.txt +++ b/backend/src/test/resources/static/nickname/adjectives.txt @@ -1 +1 @@ -춤추는,달리는,노래하는,사냥하는,지키는,전사,용감한,지혜로운,강한,빠른,조용한,헤엄치는,웃는,슬퍼하는,생각하는,꿈꾸는,사랑하는,기도하는,멋진,아름다운,소중한,힘찬,빛나는,어두운,화려한,단단한,부드러운,귀여운,강렬한,순수한,고요한,신비한,용맹한,차가운,따뜻한,반짝이는,흐르는,가벼운,무거운,흔들리는,날렵한,느린,신속한,강인한,다정한,예민한,온화한,재빠른,굳건한,우직한,유쾌한,의연한,담담한,근엄한,차분한,겸손한,헌신적인,대담한,기민한,예리한,능숙한,창의적인,도전적인,정직한,희망찬,용서하는,배려하는,진실된,정열적인,활기찬,우아한,열정적인,사려깊은,독창적인,성실한,신중한,침착한,냉철한,열렬한,엄격한,단호한,느긋한,탐구하는,분석적인,혁신적인,서있는,앉아있는,누워있는,달콤한,쌉싸름한,매콤한,향기로운,상쾌한,청량한,푸근한,촉촉한,포근한,찬란한,황홀한,짜릿한,아릿한,씩씩한,산뜻한,선명한,생생한,활발한,용기있는,모험적인,신비로운,영롱한,눈부신,고독한,슬픈,기쁜,행복한,즐거운,설레는,기대하는,뿌듯한,흐뭇한,부지런한,당당한,자신있는,평화로운,만족한,흥미로운,매혹적인,기분좋은,상냥한,긍정적인,의심하는,신뢰하는,믿음직한,든든한,편안한,안정적인,평온한,당찬,과감한,확고한,인내하는,예의바른,배려깊은,너그러운,친절한,애정있는,자비로운,은혜로운,강직한,꼼꼼한,기품있는,밝은,고운,자상한,정다운,사근한,아늑한,따사로운,생기있는,호탕한,소박한,맑은,깨끗한,명랑한,존경하는,격려하는,이끄는,희생적인,직관적인,날카로운,재치있는,명석한,영리한,현명한,이성적인,탐구적인,지적인,학구적인,학문적인,박식한,전문적인,기술적인,창조적인,예술적인,감성적인,음악적인,문학적인,철학적인,사색적인,협력적인,친화적인,공감하는,존중하는,포용적인,개방적인,유연한,민첩한,진취적인,섹시한,졸린,화난,과식하는,욕망의,뜨거운,어여쁜,재미있는,돈이많은,우등생,공부하는,밥을먹는,문제아,날아가는,숱이많은,앙증맞은,거대한,향기나는,미세한,독서광,더운,추운,시원한,적당한,네모난,날렵한,야생의,똑똑한,성공한,출세한,이타적인,야심찬,이기적인,엉뚱한,세련된,짓궂은,진지한,말이없는,명령하는,기타치는,소설쓰는,휴가간 +춤추는,달리는,노래하는,사냥하는,지키는,전사,용감한,지혜로운,강한,빠른,조용한,헤엄치는,웃는,생각하는,꿈꾸는,사랑하는,기도하는,멋진,아름다운,소중한,힘찬,빛나는,화려한,단단한,부드러운,귀여운,강렬한,순수한,고요한,신비한,용맹한,따뜻한,반짝이는,흐르는,가벼운,무거운,흔들리는,날렵한,느린,신속한,강인한,다정한,온화한,재빠른,굳건한,우직한,유쾌한,의연한,담담한,근엄한,차분한,겸손한,헌신적인,대담한,기민한,예리한,능숙한,창의적인,도전적인,정직한,희망찬,용서하는,배려하는,진실된,정열적인,활기찬,우아한,열정적인,사려깊은,독창적인,성실한,신중한,침착한,냉철한,열렬한,엄격한,단호한,느긋한,탐구하는,분석적인,혁신적인,서있는,앉아있는,누워있는,달콤한,쌉싸름한,매콤한,향기로운,상쾌한,청량한,푸근한,촉촉한,포근한,찬란한,황홀한,짜릿한,아릿한,씩씩한,산뜻한,선명한,생생한,활발한,용기있는,모험적인,신비로운,영롱한,눈부신,기쁜,행복한,즐거운,설레는,기대하는,뿌듯한,흐뭇한,부지런한,당당한,자신있는,평화로운,만족한,흥미로운,매혹적인,기분좋은,상냥한,긍정적인,의심하는,신뢰하는,믿음직한,든든한,편안한,안정적인,평온한,당찬,과감한,확고한,인내하는,예의바른,배려깊은,너그러운,친절한,애정있는,자비로운,은혜로운,강직한,꼼꼼한,기품있는,밝은,고운,자상한,정다운,사근한,아늑한,따사로운,생기있는,호탕한,소박한,맑은,깨끗한,명랑한,존경하는,격려하는,이끄는,희생적인,직관적인,날카로운,재치있는,명석한,영리한,현명한,이성적인,탐구적인,지적인,학구적인,학문적인,박식한,전문적인,기술적인,창조적인,예술적인,감성적인,음악적인,문학적인,철학적인,사색적인,협력적인,친화적인,공감하는,존중하는,포용적인,개방적인,유연한,민첩한,진취적인,어여쁜,재미있는,돈이많은,우등생,공부하는,밥먹는,날아가는,숱이많은,앙증맞은,거대한,향기나는,미세한,독서광,더운,추운,시원한,적당한,네모난,날렵한,야생의,똑똑한,성공한,출세한,이타적인,야심찬,엉뚱한,세련된,진지한,기타치는,드럼치는,소설쓰는,휴가간 \ No newline at end of file diff --git a/backend/src/test/resources/static/nickname/nouns.txt b/backend/src/test/resources/static/nickname/nouns.txt index fc3504906..838ce7fea 100644 --- a/backend/src/test/resources/static/nickname/nouns.txt +++ b/backend/src/test/resources/static/nickname/nouns.txt @@ -1 +1 @@ -도라,포케,에버,메이슨,서기,알송,채채,제이슨,제임스,토미,포비,리사,총알 +장성,온새미,용머리,루빈,고수,브로콜리,영덕,골목,열쇠,청도,영월,까마귀,하남,아크라,하노이,라즈베리,사과대추,명성산,아차산,익산,불가사리,마드리드,연암산,수련,백양산,미륵산,잠자리,호랑이,바다표범,예천,론,해국,도미,사슴벌레,케리,청량산,멜론,스피츠,체리,치커리,백곰,블랙베리,극락조,검독수리,삼광조,두더지,플래티,연꽃,눈,제리,대암산,야생딸기,산딸기,아세로라,아기돼지,에린,말라보,배추,산청딸기,시츄,향기,양,진도,바마코,휘파람새,정선,치악산,디스커스,한라산,당근,루안다,용담,시냇물,파랑,황석산,돌멩이,문수산,한탄산,바다거북,실러캔스,추억,도쿄,칸나,꽃게,구례,국사봉,콩새,도시,염소,창,게사니,릴리,카네,빅토리아,책,아라,메뚜기,개구리,조지,칸탈루프,복숭아,전복,모나코,로즈,클락새,구관조,공원,우엉,노랑자두,먼치킨,대구,직박구리,로벨,감,자카르타,강,비글,가야산,패랭이,나팔꽃,바람,청송,홍성,강물,과천,종이,페이,민들레,고릴라,물총새,싱가푸라,부여,잉어,오슬로,케인,케일,할미꽃,말미잘,이든,나래,가람,류,제이,토끼,장미,제인,달,닭,예산,커런트,망고스틴,수염고래,팔공산,톤키니즈,거북,체리바브,부천,해바라기,다람쥐,세이,감꽃,양배추,순창,김천,방콕,피타야,나비,게,돼지,햄스터,문어,원주,구절초,랫서판다,유채,은하,페키니즈,무청,블루베리,갈까마귀,베일,크리스,수달,아테네,마닐라,셀리,방울뱀,코뿔소,레인,춘천,대전,비숑,무지개,암꿩,공작,린,보르조이,도베르만,차우차우,금계,운문산,네이,아론,킨샤사,병아리,네일,펭,용인,조령산,마,안개꽃,레몬,홍학,슬기,말,던,빌뉴스,대미산,델리,맘,키위새,수리,순천,프라하,산비취,갈기늑대,꽃잎,토란,매,곰,풍란,루사카,진해,멧도요,청주,니코시아,작약,해오라기,앵무새,삼봉산,고흥,불국산,백조,두견,찌르레기,구름,부엉이,청계산,침팬지,백합,수원,후투티,팔색조,말라뮤트,아보카도,양송이,누리,이슬,소라,성주,추풍령,하늘,암탉,제드,신천옹,금산,산,올빼미,니아메,춤,골드핀치,물수리,하늬,달래,완도,메기,샐러리,퓨마,당근잎,모란,돌,실버샤크,바그다드,속리산,태백,새,내장산,우산,김포,물개,소말리,나이로비,모래,영주,의왕,봉래산,진주,앵초,보고타,바나나,파랑새,마루,라일락,부산,문경,샴,구아바,동해,레드피쉬,달빛,노래,햇살,로지바브,수국,수르남,벵갈,북한산,인형,덕항산,의정부,고래,딱따구리,비버,사천,모닥불,소피아,감악산,까막까치,천안,강진,개미,집오리,철쭉,이구아나,계절,귀,왈라비,연화산,사자산,산책,테헤란,에코,계룡산,브뤼셀,기니피그,파리,고양이,경주,접시꽃,예루살렘,라온,말티즈,조계산,장끼,류블랴나,파슬리,코스모스,귤,새싹,동고비,화순,도락산,낙엽,하마,장흥,모가디슈,바다,아이리,매화,너구리,꺼병이,황지산,갈매기,스컹크,물결,쟝,유산,다올,라브라도,보령,엔젤피쉬,판다,가온,오대산,마이산,수락산,백두산,뱀장어,보아,목련,소,카나리아,광양,손,새벽,솔,공주,고슴도치,베를린,트빌리시,치타,동백,오리엔탈,길,카트룬가,노루귀,무,카이로,거창,펄구라미,백운산,봉화,마리,군산,킹찰스,물,노아,튤립,사막,고령,두타산,푸들,성불산,별빛,울산,팬지,옐로우탱,관악산,솔바람,다리,샤페이,바다사자,루카,파도,사과,라마,족제비,오징어,크낙새,나리,담비,렉스,전갈,르나,데본렉스,학,나릿,불독,비둘기,황매산,시흥,조각,복사꽃,황병산,석류,연어,바르샤바,두륜산,나비꽃,포트비샤,평택,미나리,붕어,파란베타,바다오리,해,바위,고봉산,하이,아나콘다,조개,파인애플,해남,앵두,고양,개개비,바람개비,페더테일,함양,토니,허스키,시베리안,시내,사라예보,진달래,데이지,타일,백일홍,구월산,성남,타임,의성,무궁화,스핑크스,자스민,브라자빌,여름,천관산,청상아리,난초,서울,고창,탈린,함백산,코끼리,메리골,노루,김해,구피,트리폴리,아바나,봄베이,무화과,버마,향로산,펭귄,버만,박새,줄베타,코펜하겐,키토,이브,꾀꼬리,콩,튀니스,알리,여우,까투리,앵무,파프리카,여울,사자,리스본,토리,안산,자몽,올리브,소담,코리,라이,가을,발,자두,쿠웨이트,우주,밤,파파야,스코페,라임,안동,카피바라,백마산,다이,두루미,배,맨드라미,치와와,담양,갈대,오리,뱀,샤인,서산,고니,문조,야자,수박,가마우지,비파꽃,도담,합천,무주,바셋,물소,도르,베타,쥐,원앙,반구,종주산,달맞이,속초,도요새,강아지,거위,천일홍,무스,겨울,워싱턴,리트리버,로리,구미,크랜베리,런던,키예프,따오기,삼척,금오산,독수리,벌,실버달러,포도,로마,노리,대둔산,로드러너,고요,벤,야금,타조,까치,오타와,바라쿠다,솔개,참외,봉숭아,에메랄드,상어,이안,꿈,도마뱀,마가렛,고운,남양주,별,나소,시아,다솜,청포도,진,양양,바바리,집,꿩,고구마,어치,비안,태안,함평,슈나우저,목포,미아,나무,양귀비,에버랜드,청호반새,서귀포,아순시온,천마산,망경산,리아,때까치,리안,광명,모스크바,볼로미,청양,봄,루바브,조이,수꿩,국화,오이,암만,제비,페럿,철새,송골매,미모사,도라에몽,진악산,천황산,파주,영암,평양,페튜니,노을,글로피쉬,십자매,산티아고,신안,제비꽃,북극곰,미어캣,고운산,두견새,라스보라,백로,소백산,라벤더,벌새,베이징,날다람쥐,무등산,몰리,고라니,골담초,영양,원숭이,재규어,보성,월출산,차꽃,종다리,참새,알제,은방울,남원,루비바브,도서,수탉,프리,거제,샤프란,코기,해파리,동자꽃,리가,논산,헬싱키,소금,창원,코알라,돌고래,싱가포르,사슴,비비추,멋쟁이,고사리,연근,대추꽃,딸기,용문산,나무늘보,쟈스,라가머핀,코코넛,물범,포항,강릉,해달,울진,여수,청설모,풍조,스톡홀름,레아,포케,모기,백암산,망고,하르툼,송어,태백산,블루탱,바쿠,월악산,갈고리,대간산,박쥐,유학산,피망,수선화,늑대,라쿤,갑산,반딧불이,인천,물망초,타리꽃,양평,참치,손글,구스베리,상추,무학산,아부자,덕유산,페르시안,아로니아,밀양,주왕산,불영산,시계,운봉산,달마티안,지리산,사모예드,영취산,카트만두,그림,비,미리,빈,사바나,기러기,금강산,청경채,키위,귀촉도,도하,제네바,카라카스,산호세,바질,감자,마나마,음악,거미,리야드,반시,재기러기,양산,리마,도봉산,타이페이,전주,파리지옥,황악산,광주,제천,치산,설악산,달마시안,기린,오소리,펄다니오,새우,감귤 \ No newline at end of file diff --git a/backend/src/test/resources/static/nickname/nouns2.txt b/backend/src/test/resources/static/nickname/nouns2.txt index afded4939..c1ee4fbbc 100644 --- a/backend/src/test/resources/static/nickname/nouns2.txt +++ b/backend/src/test/resources/static/nickname/nouns2.txt @@ -1 +1 @@ -해,달,강,산,나무,바람,구름,별,불,물,꽃,새,호랑이,용,사자,고래,독수리,늑대,여우,곰,사슴,토끼,부엉이,까마귀,참새,매,황소,말,개,고양이,돼지,소,양,닭,거북이,두더지,원숭이,고릴라,코끼리,코뿔소,하마,바다,강아지,올빼미,두루미,까치,앵무새,나비,벌,개미,거미,나무늘보,고슴도치,오소리,공룡,피라미,상어,연어,새우,가재,붕어,잉어,돌고래,참치,연꽃,백합,장미,튤립,국화,해바라기,민들레,무궁화,진달래,철쭉,수선화,제비꽃,나팔꽃,달맞이꽃,제비,학,봉황,비둘기,갈매기,파랑새,물총새,갈색곰,팬더,미어캣,플라밍고,백조,매미,방울새,강산,초원,사막,폭포,숲,눈,우주,천둥,번개,저녁,아침,새벽,황혼,새벽녘,보름달,은하수,해돋이,해질녘,태양,소나기,땅,언덕,계곡,늪,목초지,사파리,정글,밀림,산맥,협곡,절벽,해안,해변,모래사장,바위,암석,산호,해조류,유령,신령,선녀,도깨비,요정,천사,악마,영혼,망령,정령,초록,파랑,빨강,노랑,주황,보라,분홍,회색,흰색,검정,금색,은색,청록,연두,다홍,진홍,남색,청색,미색,담홍,담청,옥색,주황색,갈색,하늘색,청명,무지개,해골,드래곤,유니콘,피닉스,세이렌,메두사,페가수스,히드라,키메라,하피,그리핀,드라큘라,늑대인간,좀비,스켈레톤,레이스,벤시,오로라,자작나무,붉은노을,파도,용암,황금빛,아지랑이,서리,이슬,메아리,흙,잎사귀,뿌리,가시,씨앗,모래,산들바람,비,우박,눈보라,폭풍,폭우,장마,노을,여명,적막,어둠,맑음,흐림,안개,연무,먼지,태풍,허리케인,모래바람,진눈깨비,미풍,강풍,돌풍,눈발,일몰,일출,청둥오리,원앙,왜가리,황새,갈대,억새,연못,호수,시냇물,개천,웅덩이,동굴,바위산,평원,사바나,초지,숲속,정원,공원,대나무숲,잔디밭,유채꽃,벚꽃,라일락,수국,작약,모란,천리향,매화,목련,감나무,배나무,사과나무,포도나무,레몬나무,밤나무,호두나무,은행나무,소나무,참나무,쿼카,악어,기린,오리,너구리,휴먼,침팬지,홍학,가마우지,카멜레온,달팽이,구렁이,이무기,얼룩말,불사조,디멘터,하이에나,맘모스,랩터,햄스터,치타,익룡,멧돼지,산돼지,피글렛,캥거루,산토끼,쥐,기니피그,시골쥐,도시쥐,패럿,수달,북극곰,펭귄,남극곰,밍크,족제비,뱀,코브라,아나콘다,킹코브라,담비,타조,북극여우,오랑우탄,물범,코알라,하프물범,북극토끼,칠면조,직박구리,황제펭귄,물개,판다,랫서판다,범고래,식인고래,개구리,물소,맹꽁이,우파루파,이구아나,염소,노새,당나귀,올챙이,병아리,살모사,도롱뇽,퓨마,다람쥐,알파카,진돗개,웰시코기,말티즈,닥스훈트,리트리버,푸들,낙타,스피츠,삽살개,먼치킨,래그돌,공작새,쥐며느리,키위새,죠스,식인상어,아기상어,자라,표범,청설모,바비루사,빅풋,예티,메추라기,스라소니,삵,카피바라,라마,딱따구리,기러기,스컹크,해태,구미호,인면조,개미핥기,갑오징어,두억시니,샐러맨더,와이번,다오,마리드,배찌,디지니,우니,에띠,케피,로두마니,모스,마리오,루이지,피치,로젤리나,쿠파,키노피오,와리오,사일러스,헤카림,진,가렌,갈리오,갱플랭크,그라가스,나르,나미,나서스,노틸러스,녹턴,누누,니달리,다리우스,다이애나,드레이븐,라이즈,라칸,람머스,럭스,럼블,레넥톤,레오나,렉사이,렐,그웬,렝가,루시안,룰루,르블랑,리신,리븐,바드,미스포츈,문도박사,마스터이,마오카이,말파이트,볼리베어,브라움,모르가나,브랜드,비에고,빅토르,사미라,사이온,샤코,세나,세라핀,세주아니,세트,아리,아무무,신드라,시비르,신짜오,스카너,아이번,아지르,소라카,소나,쉔,애니비아,겐지,둠피스트,리퍼,맥크리,메이,바스티온,솜브라,시메트라,애쉬,에코,정크랫,토르비욘,트레이서,파라,한조,레킹볼,로드호그,시그마,오리사,윈스턴,자리야,루시우,메르시,모이라,바티스트,브리기테,아나,젠야타,방갈로르,미라지,옥테인,레버넌트,호라이즌,퓨즈,크립토,발키리,로바,지브롤터,코스틱,왓슨,램파트,소닉,테일즈,스랄,제이나,가로쉬,데스윙,발리라,마이에브,우서,렉사르,실바나스,말퓨리온,굴단,느조스,메디브,안두인,일리단,피즈,피오라,피들스틱,판테온,파이크,티모,트위치,트런들,고든,탐켄치,탈리야,탈론,타릭,킨드레드,키아나,클레드,퀸,코르키,코그모,케일,케인,케이틀린,케넨,칼리스타,카타리나,카직스,카이사,카서스,카사딘,카밀,카르마,초가스,징크스,질리언,직스,조이,제이스,제라스,제드,잭스,잔나,자크,자르반,일라오이,이즈리얼,이블린,이렐리아,유미,워윅,우르곳,우디르,요릭,요네,올라프,오른,오리아나,오공,엘리스,야스오,애니,알리스타,아트록스,아칼리,아크샨,마린,파이어뱃,벌쳐,시즈탱크,저글링,럴커,메딕,고스트,리버,옵저버,스카웃,캐리어,질럿,아칸,다크아칸,드라군,커세어,스커지,디바우러,가디언,공허충,말자하,카즈야,헤이하치,에디,알리사,간류,니나,안나,리리,브라이언,샤오유,아머킹,요시미츠,자피나,쿠니미츠,화랑,아이작,피터파커,해피호건,페퍼포츠,쟈비스,쿠엔틴벡,호인센,이반반코,페기카터,베티로스,릭메이슨,로라바튼,행크핌,캐시랭,루이스,메이파커,네드,리즈,비전,샘윌슨,스콧랭,트촬라,버키반즈,슈리,은죠부,오코예,나키아,음바쿠,쥬리,웡,칼모르도,도르마무,맷머독,빌리루소,울트론,헬라,헤임달,피터퀼,가모라,드랙스,로켓라쿤,그루트,맨티스,네뷸라,타노스,길가메쉬,스타폭스,킨고,에이잭,치타우리,에고,노웨어,닉퓨리,필콜슨,마리아힐,마야한센,샹치,만다린,드루이그,리오피츠,정복자캉,트촤카,로난,파이리,꼬부기,피카츄,라이츄,버터플,이상해씨,리자몽,거북왕,캐터피,독침붕,피죤,꼬렛,구구,아보,모래두지,고지,니드퀸,니드킹,식스테일,나인테일,픽시,삐삐,주뱃,푸린,뚜벅쵸,디그다,고라파덕,성원숭,윈디,발챙이,슈륙챙이,케이시,윤겔라,알통몬,우츠동,모다피,왕눈해,꼬마돌,롱스톤,야돈,코일,파오리,두두,쥬쥬,질퍽이,파르셀,고오스,팬텀,슬리퍼,크랩,킹크랩,찌리리공,붐볼,아라리,나시,탕구리,홍수몬,시라소몬,내루미,또가스,코뿌리,럭키,쏘드라,콘치,별가사리,아쿠스타,마임맨,스라크,루주라,마그마,잉어킹,갸라도스,라프라스,메타몽,이브이,쥬피썬더,폴리곤,부스터,투구푸스,프테라,잠만보,프리져,썬더,미뇽,망나뇽,뮤츠,뮤,치코리타,토게피,세레비,칠색조,뿔카노,에레브,쁘사이저,켄타로스,샤미드,암나이트,신뇽,질뻐기 +도라,포케,에버,메이슨,서기,알송,채채,제이슨,제임스,토미,포비,리사,총알 \ No newline at end of file