diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java index 3f9aa0324..69cd6f204 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java @@ -93,9 +93,9 @@ public TokenResponse validVerificationMessage( throw new CertificationNumberNotMatchException(); } - updateToVerifiedMember(memberId, plain); + final Long verifiedMemberId = updateToVerifiedMember(memberId, plain); - return tokenManager.createNewToken(memberId); + return tokenManager.createNewToken(verifiedMemberId); } private boolean isNotMatch(final String certificationNumber, @@ -103,15 +103,17 @@ private boolean isNotMatch(final String certificationNumber, return !certificationNumber.equals(findCertificationNumber); } - private void updateToVerifiedMember(final Long memberId, final byte[] plain) { + private Long updateToVerifiedMember(final Long memberId, final byte[] plain) { final MemberTemporary memberTemporary = memberTemporaryRepository.findById(memberId) .orElseThrow(MemberNotFoundException::new); memberTemporaryRepository.delete(memberTemporary); - final Member member = memberTemporary.toMember(hashEncryptionManager.encrypt(plain), + final Member verifiedMember = memberTemporary.toMember(hashEncryptionManager.encrypt(plain), aesEncryptionManager.encryptWithPrefixIV(plain)); - memberRepository.save(member); + memberRepository.save(verifiedMember); + + return verifiedMember.getId(); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java index 66eaa0398..fe4777a1d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java @@ -34,6 +34,7 @@ public record NearbyARCapsuleSummaryResponse( @Schema(description = "캡슐 타입") CapsuleType capsuleType ) { + public NearbyARCapsuleSummaryResponse { if (dueDate != null) { dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java index 4f5378f46..03908bba4 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java @@ -19,7 +19,7 @@ Optional findCapsuleByMemberIdAndCapsuleId( @Modifying(clearAutomatically = true) @Query("UPDATE Capsule c SET c.isOpened = true WHERE c.id = :capsuleId and c.member.id = :memberId") - void updateIsOpenedTrue( + int updateIsOpenedTrue( @Param("memberId") Long memberId, @Param("capsuleId") Long capsuleId ); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java index f1414845b..150af31c1 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java @@ -74,7 +74,11 @@ public Capsule findCapsuleByMemberIdAndCapsuleId(final Long memberId, final Long @Transactional public void updateIsOpenedTrue(final Long memberId, final Long capsuleId) { - capsuleRepository.updateIsOpenedTrue(memberId, capsuleId); + int isOpenedTrue = capsuleRepository.updateIsOpenedTrue(memberId, capsuleId); + + if (isOpenedTrue != 1) { + throw new CapsuleNotFondException(); + } } @Transactional diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java index 9b3c37704..19d661802 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java @@ -4,7 +4,6 @@ import java.util.function.Function; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.secret_capsule.data.response.MySecreteCapsuleResponse; -import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; public record MySecreteCapsuleDto( Long capsuleId, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java index 436c43636..e42ccca02 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java @@ -12,7 +12,6 @@ import org.springframework.http.ResponseEntity; import site.timecapsulearchive.core.domain.friend.data.request.SearchFriendsRequest; import site.timecapsulearchive.core.domain.friend.data.request.SendFriendRequest; -import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendRequestsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchFriendsResponse; @@ -129,7 +128,7 @@ ResponseEntity> denyFriendRequest( description = "외부 API 요청 실패" ) }) - ResponseEntity> requestFriend( + ResponseEntity> requestFriend( Long memberId, @Parameter(in = ParameterIn.PATH, description = "친구 아이디", required = true, schema = @Schema()) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java index 56c2e0755..3eb3fc44c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java @@ -19,7 +19,6 @@ import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.request.SearchFriendsRequest; import site.timecapsulearchive.core.domain.friend.data.request.SendFriendRequest; -import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendRequestsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchFriendsResponse; @@ -89,7 +88,9 @@ public ResponseEntity> deleteFriend( friendService.deleteFriend(memberId, friendId); return ResponseEntity.ok( - ApiSpec.empty(SuccessCode.SUCCESS) + ApiSpec.empty( + SuccessCode.SUCCESS + ) ); } @@ -101,23 +102,26 @@ public ResponseEntity> denyFriendRequest( friendService.denyRequestFriend(memberId, friendId); - return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); - + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); } @PostMapping(value = "/{friend_id}/request") @Override - public ResponseEntity> requestFriend( + public ResponseEntity> requestFriend( @AuthenticationPrincipal final Long memberId, @PathVariable("friend_id") final Long friendId) { - return ResponseEntity.accepted() - .body( - ApiSpec.success( - SuccessCode.SUCCESS, - friendService.requestFriend(memberId, friendId) - ) - ); + friendService.requestFriend(memberId, friendId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @PostMapping(value = "/requests") @@ -128,8 +132,11 @@ public ResponseEntity> requestFriends( ) { friendFacade.requestFriends(memberId, request.friendIds()); - return ResponseEntity.accepted() - .body(ApiSpec.empty(SuccessCode.ACCEPTED)); + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @PostMapping(value = "/{friend_id}/accept-request") @@ -140,7 +147,11 @@ public ResponseEntity> acceptFriendRequest( ) { friendService.acceptFriend(memberId, friendId); - return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); } @PostMapping( @@ -173,12 +184,11 @@ public ResponseEntity> searchFriendByTag @AuthenticationPrincipal Long memberId, @RequestParam(value = "friend_tag") final String tag ) { - return ResponseEntity.ok() - .body( - ApiSpec.success( - SuccessCode.SUCCESS, - friendService.searchFriend(memberId, tag) - ) - ); + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + friendService.searchFriend(memberId, tag) + ) + ); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendReqStatusResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendReqStatusResponse.java deleted file mode 100644 index 307da6a73..000000000 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendReqStatusResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package site.timecapsulearchive.core.domain.friend.data.response; - -import org.springframework.http.HttpStatus; - -public record FriendReqStatusResponse( - HttpStatus httpStatus, - String result -) { - - public static FriendReqStatusResponse success() { - return new FriendReqStatusResponse(HttpStatus.ACCEPTED, "친구 요청 메시지 전송 성공!"); - } -} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java index 640c2077d..3e6859eea 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java @@ -2,7 +2,9 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; +import java.time.ZonedDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.BatchPreparedStatementSetter; @@ -19,8 +21,8 @@ public void bulkSave(final Long ownerId, final List friendIds) { jdbcTemplate.batchUpdate( """ INSERT INTO friend_invite ( - friend_invite_id, owner_id, friend_id - ) values (?, ?, ?) + friend_invite_id, owner_id, friend_id, created_at, updated_at + ) values (?, ?, ?, ?, ?) """, new BatchPreparedStatementSetter() { @@ -30,6 +32,8 @@ public void setValues(final PreparedStatement ps, final int i) throws SQLExcepti ps.setNull(1, Types.BIGINT); ps.setLong(2, ownerId); ps.setLong(3, friendId); + ps.setTimestamp(4, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + ps.setTimestamp(5, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java index c7c94a40a..df8643a77 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java @@ -24,7 +24,7 @@ List findFriendInviteWithMembersByOwnerIdAndFriendId( Optional findFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); - void deleteFriendInviteById(Long id); + int deleteFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); void delete(FriendInvite friendInvite); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java index 913700af2..68fb8cf6f 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java @@ -13,7 +13,6 @@ import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; -import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchTagFriendSummaryResponse; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; @@ -43,7 +42,7 @@ public class FriendService { private final SocialNotificationManager socialNotificationManager; private final TransactionTemplate transactionTemplate; - public FriendReqStatusResponse requestFriend(final Long memberId, final Long friendId) { + public void requestFriend(final Long memberId, final Long friendId) { validateFriendDuplicateId(memberId, friendId); validateTwoWayInvite(memberId, friendId); @@ -63,8 +62,6 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { }); socialNotificationManager.sendFriendReqMessage(owner.getNickname(), friendId); - - return FriendReqStatusResponse.success(); } private void validateFriendDuplicateId(final Long memberId, final Long friendId) { @@ -114,20 +111,22 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { } @Transactional - public void denyRequestFriend(Long memberId, Long friendId) { + public void denyRequestFriend(final Long memberId, final Long friendId) { validateFriendDuplicateId(memberId, friendId); - final FriendInvite friendInvite = friendInviteRepository - .findFriendInviteByOwnerIdAndFriendId(memberId, friendId).orElseThrow( - FriendNotFoundException::new); - friendInviteRepository.deleteFriendInviteById(friendInvite.getId()); + int isDenyRequest = friendInviteRepository.deleteFriendInviteByOwnerIdAndFriendId(friendId, + memberId); + + if (isDenyRequest != 1) { + throw new FriendTwoWayInviteException(); + } } @Transactional public void deleteFriend(final Long memberId, final Long friendId) { validateFriendDuplicateId(memberId, friendId); - List memberFriends = memberFriendRepository + final List memberFriends = memberFriendRepository .findMemberFriendByOwnerIdAndFriendId(memberId, friendId); memberFriends.forEach(memberFriendRepository::delete); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java index d90e76ef5..7f79e682a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java @@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupCreateRequest; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupUpdateRequest; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; @@ -32,15 +31,24 @@ public interface GroupApi { @ApiResponse( responseCode = "200", description = "처리 완료" + ), + @ApiResponse( + responseCode = "404", + description = "그룹 초대 찾기 실패" + ), + @ApiResponse( + responseCode = "500", + description = "외부 API 요청 실패" ) }) - @PostMapping(value = "/groups/{group_id}/members/{member_id}/accept-invitation") - ResponseEntity acceptGroupInvitation( - @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + ResponseEntity> acceptGroupInvitation( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true, schema = @Schema()) - @PathVariable("member_id") Long memberId + @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true) + Long groupId, + + @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true) + Long targetId ); @Operation( @@ -53,6 +61,10 @@ ResponseEntity acceptGroupInvitation( @ApiResponse( responseCode = "200", description = "처리완료" + ), + @ApiResponse( + responseCode = "500", + description = "외부 API 요청 실패" ) }) ResponseEntity> createGroup( @@ -111,13 +123,13 @@ ResponseEntity deleteGroupMember( description = "처리 완료" ) }) - @PostMapping(value = "/groups/{group_id}/members/{member_id}/deny-invitation") - ResponseEntity denyGroupInvitation( - @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + ResponseEntity> rejectGroupInvitation( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true, schema = @Schema()) - @PathVariable("member_id") Long memberId + Long groupId, + + @Parameter(in = ParameterIn.PATH, description = "그룹 초대 대상 아이디", required = true) + Long groupOwnerId ); @Operation( @@ -181,13 +193,14 @@ ResponseEntity> findGroups( description = "처리 시작" ) }) - @PostMapping(value = "/groups/{group_id}/members/{member_id}/invitation") - ResponseEntity inviteGroup( - @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + ResponseEntity> inviteGroup( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true, schema = @Schema()) - @PathVariable("member_id") Long memberId + @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true) + Long groupId, + + @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true) + Long targetId ); @Operation( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java index e8a067e35..3efb49539 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -36,9 +37,20 @@ public class GroupApiController implements GroupApi { private final S3UrlGenerator s3UrlGenerator; private final S3PreSignedUrlManager s3PreSignedUrlManager; + @PostMapping(value = "/accept/{group_id}/member/{target_id}") @Override - public ResponseEntity acceptGroupInvitation(Long groupId, Long memberId) { - return null; + public ResponseEntity> acceptGroupInvitation( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId, + @PathVariable("target_id") final Long targetId + ) { + groupService.acceptGroupInvite(memberId, groupId, targetId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @Override @@ -55,7 +67,7 @@ public ResponseEntity> createGroup( return ResponseEntity.ok( ApiSpec.empty( - SuccessCode.SUCCESS + SuccessCode.ACCEPTED ) ); } @@ -70,9 +82,19 @@ public ResponseEntity deleteGroupMember(Long groupId, Long memberId) { return null; } - @Override - public ResponseEntity denyGroupInvitation(Long groupId, Long memberId) { - return null; + @DeleteMapping(value = "/reject/{group_id}/member/{target_id}") + public ResponseEntity> rejectGroupInvitation( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId, + @PathVariable("target_id") final Long targetId) { + + groupService.rejectRequestGroup(memberId, groupId, targetId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); } @GetMapping( @@ -84,7 +106,8 @@ public ResponseEntity> findGroupDetailById( @AuthenticationPrincipal final Long memberId, @PathVariable("group_id") final Long groupId ) { - final GroupDetailDto groupDetailDto = groupService.findGroupDetailByGroupId(memberId, groupId); + final GroupDetailDto groupDetailDto = groupService.findGroupDetailByGroupId(memberId, + groupId); return ResponseEntity.ok( ApiSpec.success( @@ -118,9 +141,20 @@ public ResponseEntity> findGroups( ); } + @PostMapping(value = "/invite/{group_id}/member/{target_id}") @Override - public ResponseEntity inviteGroup(Long groupId, Long memberId) { - return null; + public ResponseEntity> inviteGroup( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId, + @PathVariable("target_id") final Long targetId + ) { + groupService.inviteGroup(memberId, groupId, targetId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java index c45a8a46e..d97da178e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java @@ -5,7 +5,6 @@ import java.util.function.Function; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; import site.timecapsulearchive.core.domain.group.data.response.GroupMemberResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; public record GroupDetailDto( String groupName, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupOwnerSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupOwnerSummaryDto.java new file mode 100644 index 000000000..3496d3ec8 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupOwnerSummaryDto.java @@ -0,0 +1,9 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +public record GroupOwnerSummaryDto( + String nickname, + Boolean isOwner, + String groupProfileUrl +) { + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java index 41c184b35..690014c01 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java @@ -26,8 +26,25 @@ public class GroupInvite extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "group_id", nullable = false) + private Long groupId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_owner_id", nullable = false) + private Member groupOwner; + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; + @JoinColumn(name = "group_member_id", nullable = false) + private Member groupMember; + + private GroupInvite(Long groupId, Member groupOwner, Member groupMember) { + this.groupId = groupId; + this.groupOwner = groupOwner; + this.groupMember = groupMember; + } + + public static GroupInvite createOf(Long groupId, Member groupOwner, Member groupMember) { + return new GroupInvite(groupId, groupOwner, groupMember); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java index d9c4f5192..67c37404c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java @@ -46,4 +46,8 @@ private MemberGroup(Boolean isOwner, Member member, Group group) { public static MemberGroup createGroupOwner(Member member, Group group) { return new MemberGroup(true, member, group); } + + public static MemberGroup createGroupMember(Member member, Group group) { + return new MemberGroup(false, member, group); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupInviteNotFoundException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupInviteNotFoundException.java new file mode 100644 index 000000000..39ad8bb61 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupInviteNotFoundException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.group.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class GroupInviteNotFoundException extends BusinessException { + + public GroupInviteNotFoundException() { + super(ErrorCode.GROUP_INVITATION_NOT_FOUND_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupOwnerAuthenticateException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupOwnerAuthenticateException.java new file mode 100644 index 000000000..2c6382b85 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupOwnerAuthenticateException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.group.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class GroupOwnerAuthenticateException extends BusinessException { + + public GroupOwnerAuthenticateException() { + super(ErrorCode.GROUP_OWNER_AUTHENTICATE_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupInviteRepository.java deleted file mode 100644 index edd27e243..000000000 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupInviteRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package site.timecapsulearchive.core.domain.group.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import site.timecapsulearchive.core.domain.group.entity.GroupInvite; - -public interface GroupInviteRepository extends JpaRepository { - -} - diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepository.java new file mode 100644 index 000000000..757acc930 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepository.java @@ -0,0 +1,8 @@ +package site.timecapsulearchive.core.domain.group.repository.groupInviteRepository; + +import java.util.List; + +public interface GroupInviteQueryRepository { + + void bulkSave(final Long groupOwnerId, final List groupMemberIds); +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepositoryImpl.java new file mode 100644 index 000000000..8d71332c7 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepositoryImpl.java @@ -0,0 +1,48 @@ +package site.timecapsulearchive.core.domain.group.repository.groupInviteRepository; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class GroupInviteQueryRepositoryImpl implements GroupInviteQueryRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void bulkSave(Long groupOwnerId, List groupMemberIds) { + jdbcTemplate.batchUpdate( + """ + INSERT INTO group_invite ( + group_invite_id, group_owner_id, group_member_id, created_at, updated_at + ) values (?, ?, ?, ?, ?) + """, + new BatchPreparedStatementSetter() { + + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final Long groupMember = groupMemberIds.get(i); + ps.setNull(1, Types.BIGINT); + ps.setLong(2, groupOwnerId); + ps.setLong(3, groupMember); + ps.setTimestamp(4, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + ps.setTimestamp(5, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + } + + @Override + public int getBatchSize() { + return groupMemberIds.size(); + } + } + ); + } + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteRepository.java new file mode 100644 index 000000000..1331805dd --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteRepository.java @@ -0,0 +1,14 @@ +package site.timecapsulearchive.core.domain.group.repository.groupInviteRepository; + +import org.springframework.data.repository.Repository; +import site.timecapsulearchive.core.domain.group.entity.GroupInvite; + +public interface GroupInviteRepository extends Repository, + GroupInviteQueryRepository { + + void save(GroupInvite groupInvite); + + int deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId(Long groupId, Long groupOwnerId, + Long groupMemberId); +} + diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepository.java new file mode 100644 index 000000000..0ce6327a9 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepository.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.domain.group.repository.groupRepository; + +import java.time.ZonedDateTime; +import java.util.Optional; +import org.springframework.data.domain.Slice; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; + + +public interface GroupQueryRepository { + + Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ); + + Optional findGroupDetailByGroupId(final Long groupId); + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepositoryImpl.java similarity index 95% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepositoryImpl.java index 9b7e5a201..21ff3e10b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepositoryImpl.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.group.repository; +package site.timecapsulearchive.core.domain.group.repository.groupRepository; import static com.querydsl.core.group.GroupBy.groupBy; import static com.querydsl.core.group.GroupBy.list; @@ -22,7 +22,7 @@ @Repository @RequiredArgsConstructor -public class GroupQueryRepository { +public class GroupQueryRepositoryImpl implements GroupQueryRepository { private final JPAQueryFactory jpaQueryFactory; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupRepository.java similarity index 73% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupRepository.java index 383258106..24eb4c0c7 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupRepository.java @@ -1,10 +1,10 @@ -package site.timecapsulearchive.core.domain.group.repository; +package site.timecapsulearchive.core.domain.group.repository.groupRepository; import java.util.Optional; import org.springframework.data.repository.Repository; import site.timecapsulearchive.core.domain.group.entity.Group; -public interface GroupRepository extends Repository { +public interface GroupRepository extends Repository, GroupQueryRepository { void save(Group group); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepository.java new file mode 100644 index 000000000..c39ef58b2 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepository.java @@ -0,0 +1,10 @@ +package site.timecapsulearchive.core.domain.group.repository.memberGroupRepository; + +import java.util.Optional; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; + +public interface MemberGroupQueryRepository { + + Optional findOwnerInMemberGroup(Long groupId, Long memberId); + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepositoryImpl.java new file mode 100644 index 000000000..6e240515a --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepositoryImpl.java @@ -0,0 +1,40 @@ +package site.timecapsulearchive.core.domain.group.repository.memberGroupRepository; + +import static site.timecapsulearchive.core.domain.group.entity.QGroup.group; +import static site.timecapsulearchive.core.domain.group.entity.QMemberGroup.memberGroup; +import static site.timecapsulearchive.core.domain.member.entity.QMember.member; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; + +@Repository +@RequiredArgsConstructor +public class MemberGroupQueryRepositoryImpl implements MemberGroupQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Optional findOwnerInMemberGroup(final Long groupId, + final Long memberId) { + return Optional.ofNullable(jpaQueryFactory + .select( + Projections.constructor( + GroupOwnerSummaryDto.class, + member.nickname, + memberGroup.isOwner, + group.groupProfileUrl + ) + ) + .from(memberGroup) + .join(memberGroup.member, member) + .join(memberGroup.group, group) + .where(memberGroup.group.id.eq(groupId) + .and(memberGroup.member.id.eq(memberId))) + .fetchFirst() + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupRepository.java similarity index 64% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupRepository.java index b5e46ade4..d5ded67da 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupRepository.java @@ -1,9 +1,10 @@ -package site.timecapsulearchive.core.domain.group.repository; +package site.timecapsulearchive.core.domain.group.repository.memberGroupRepository; import org.springframework.data.repository.Repository; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; -public interface MemberGroupRepository extends Repository { +public interface MemberGroupRepository extends Repository, + MemberGroupQueryRepository { void save(MemberGroup memberGroup); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java index 98b3b1603..5fa63bedf 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java @@ -1,87 +1,8 @@ package site.timecapsulearchive.core.domain.group.service; -import lombok.RequiredArgsConstructor; -import java.time.ZonedDateTime; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; -import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; -import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; -import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; -import site.timecapsulearchive.core.domain.group.entity.Group; -import site.timecapsulearchive.core.domain.group.entity.MemberGroup; -import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; -import site.timecapsulearchive.core.domain.group.repository.GroupQueryRepository; -import site.timecapsulearchive.core.domain.group.repository.GroupRepository; -import site.timecapsulearchive.core.domain.group.repository.MemberGroupRepository; -import site.timecapsulearchive.core.domain.member.entity.Member; -import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; -import site.timecapsulearchive.core.domain.member.repository.MemberRepository; -import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; +import site.timecapsulearchive.core.domain.group.service.read.GroupReadService; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteService; -@Service -@RequiredArgsConstructor -public class GroupService { +public interface GroupService extends GroupReadService, GroupWriteService { - private final GroupRepository groupRepository; - private final MemberRepository memberRepository; - private final MemberGroupRepository memberGroupRepository; - private final TransactionTemplate transactionTemplate; - private final SocialNotificationManager socialNotificationManager; - private final GroupQueryRepository groupQueryRepository; - - public void createGroup(final Long memberId, final GroupCreateDto dto) { - final Member member = memberRepository.findMemberById(memberId) - .orElseThrow(MemberNotFoundException::new); - - final Group group = dto.toEntity(); - - final MemberGroup memberGroup = MemberGroup.createGroupOwner(member, group); - - transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - groupRepository.save(group); - memberGroupRepository.save(memberGroup); - } - }); - - socialNotificationManager.sendGroupInviteMessage(member.getNickname(), - dto.groupProfileUrl(), dto.targetIds()); - } - - @Transactional(readOnly = true) - public Group findGroupById(Long groupId) { - return groupRepository.findGroupById(groupId) - .orElseThrow(GroupNotFoundException::new); - } - - @Transactional(readOnly = true) - public Slice findGroupsSlice( - final Long memberId, - final int size, - final ZonedDateTime createdAt - ) { - return groupQueryRepository.findGroupsSlice(memberId, size, createdAt); - } - - @Transactional(readOnly = true) - public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { - final GroupDetailDto groupDetailDto = groupQueryRepository.findGroupDetailByGroupId(groupId) - .orElseThrow(GroupNotFoundException::new); - - final boolean isGroupMember = groupDetailDto.members() - .stream() - .anyMatch(m -> m.memberId().equals(memberId)); - - if (!isGroupMember) { - throw new GroupNotFoundException(); - } - - return groupDetailDto; - } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupServiceImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupServiceImpl.java new file mode 100644 index 000000000..b1252d0ea --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupServiceImpl.java @@ -0,0 +1,60 @@ +package site.timecapsulearchive.core.domain.group.service; + +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.service.read.GroupReadService; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteService; + +@Service +@RequiredArgsConstructor +public class GroupServiceImpl implements GroupService { + + private final GroupReadService groupReadService; + private final GroupWriteService groupWriteService; + + @Override + public Group findGroupById(final Long groupId) { + return groupReadService.findGroupById(groupId); + } + + @Override + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupReadService.findGroupsSlice(memberId, size, createdAt); + } + + @Override + public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { + return groupReadService.findGroupDetailByGroupId(memberId, groupId); + } + + @Override + public void createGroup(final Long memberId, final GroupCreateDto dto) { + groupWriteService.createGroup(memberId, dto); + } + + @Override + public void inviteGroup(final Long memberId, final Long groupId, final Long targetId) { + groupWriteService.inviteGroup(memberId, groupId, targetId); + } + + @Override + public void rejectRequestGroup(final Long groupMemberId, final Long groupId, + final Long targetId) { + groupWriteService.rejectRequestGroup(groupMemberId, groupId, targetId); + } + + @Override + public void acceptGroupInvite(final Long memberId, final Long groupId, final Long targetId) { + groupWriteService.acceptGroupInvite(memberId, groupId, targetId); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadService.java new file mode 100644 index 000000000..635b1dbb1 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadService.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.domain.group.service.read; + +import java.time.ZonedDateTime; +import org.springframework.data.domain.Slice; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; + +public interface GroupReadService { + + Group findGroupById(final Long groupId); + + Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ); + + GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId); +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadServiceImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadServiceImpl.java new file mode 100644 index 000000000..d3066d287 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadServiceImpl.java @@ -0,0 +1,48 @@ +package site.timecapsulearchive.core.domain.group.service.read; + +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GroupReadServiceImpl implements GroupReadService { + + private final GroupRepository groupRepository; + + public Group findGroupById(final Long groupId) { + return groupRepository.findGroupById(groupId) + .orElseThrow(GroupNotFoundException::new); + } + + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupRepository.findGroupsSlice(memberId, size, createdAt); + } + + public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { + final GroupDetailDto groupDetailDto = groupRepository.findGroupDetailByGroupId(groupId) + .orElseThrow(GroupNotFoundException::new); + + final boolean isGroupMember = groupDetailDto.members() + .stream() + .anyMatch(m -> m.memberId().equals(memberId)); + + if (!isGroupMember) { + throw new GroupNotFoundException(); + } + + return groupDetailDto; + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteService.java new file mode 100644 index 000000000..6eb08e6f6 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteService.java @@ -0,0 +1,15 @@ +package site.timecapsulearchive.core.domain.group.service.write; + +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; + +public interface GroupWriteService { + + void createGroup(final Long memberId, final GroupCreateDto dto); + + void inviteGroup(final Long memberId, final Long groupId, final Long targetId); + + void rejectRequestGroup(final Long memberId, final Long groupId, final Long targetId); + + void acceptGroupInvite(final Long memberId, final Long groupId, final Long targetId); + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteServiceImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteServiceImpl.java new file mode 100644 index 000000000..b2065bee5 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteServiceImpl.java @@ -0,0 +1,121 @@ +package site.timecapsulearchive.core.domain.group.service.write; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.GroupInvite; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.group.exception.GroupInviteNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupOwnerAuthenticateException; +import site.timecapsulearchive.core.domain.group.repository.groupInviteRepository.GroupInviteRepository; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupRepository; +import site.timecapsulearchive.core.domain.group.repository.memberGroupRepository.MemberGroupRepository; +import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; +import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; + +@Service +@RequiredArgsConstructor +public class GroupWriteServiceImpl implements GroupWriteService { + + private final MemberRepository memberRepository; + private final GroupRepository groupRepository; + private final MemberGroupRepository memberGroupRepository; + private final GroupInviteRepository groupInviteRepository; + private final TransactionTemplate transactionTemplate; + private final SocialNotificationManager socialNotificationManager; + + public void createGroup(final Long memberId, final GroupCreateDto dto) { + final Member member = memberRepository.findMemberById(memberId) + .orElseThrow(MemberNotFoundException::new); + + final Group group = dto.toEntity(); + + final MemberGroup memberGroup = MemberGroup.createGroupOwner(member, group); + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + groupRepository.save(group); + memberGroupRepository.save(memberGroup); + groupInviteRepository.bulkSave(memberId, dto.targetIds()); + } + }); + + socialNotificationManager.sendGroupInviteMessage(member.getNickname(), + dto.groupProfileUrl(), dto.targetIds()); + } + + public void inviteGroup(final Long memberId, final Long groupId, final Long targetId) { + final Member groupOwner = memberRepository.findMemberById(memberId).orElseThrow( + MemberNotFoundException::new); + + final Member groupMember = memberRepository.findMemberById(targetId).orElseThrow( + MemberNotFoundException::new); + + final GroupInvite groupInvite = GroupInvite.createOf(groupId, groupOwner, groupMember); + + final GroupOwnerSummaryDto[] summaryDto = new GroupOwnerSummaryDto[1]; + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + summaryDto[0] = memberGroupRepository.findOwnerInMemberGroup( + groupId, memberId).orElseThrow(GroupNotFoundException::new); + + if (!summaryDto[0].isOwner()) { + throw new GroupOwnerAuthenticateException(); + } + + groupInviteRepository.save(groupInvite); + } + }); + + socialNotificationManager.sendGroupInviteMessage(summaryDto[0].nickname(), + summaryDto[0].groupProfileUrl(), List.of(targetId)); + } + + @Transactional + public void rejectRequestGroup(final Long memberId, final Long groupId, final Long targetId) { + final int isDenyRequest = groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId); + + if (isDenyRequest != 1) { + throw new GroupInviteNotFoundException(); + } + } + + public void acceptGroupInvite(final Long memberId, final Long groupId, final Long targetId) { + final Member groupMember = memberRepository.findMemberById(memberId) + .orElseThrow(MemberNotFoundException::new); + final Group group = groupRepository.findGroupById(groupId) + .orElseThrow(GroupNotFoundException::new); + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + final int isDenyRequest = groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId); + + if (isDenyRequest != 1) { + throw new GroupInviteNotFoundException(); + } + + memberGroupRepository.save(MemberGroup.createGroupMember(groupMember, group)); + } + }); + + socialNotificationManager.sendGroupAcceptMessage(groupMember.getNickname(), targetId); + } + + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java index 730084363..a99bd8fff 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java @@ -19,7 +19,6 @@ import site.timecapsulearchive.core.domain.capsule.entity.Capsule; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; -import site.timecapsulearchive.core.domain.group.entity.GroupInvite; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; import site.timecapsulearchive.core.domain.history.entity.History; import site.timecapsulearchive.core.global.entity.BaseEntity; @@ -77,9 +76,6 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List capsules; - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List groupInvites; - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List groups; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java index 62042ca10..9e8f9b31d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java @@ -26,6 +26,4 @@ int updateMemberNotificationEnabled( @Param("memberId") Long memberId, @Param("notificationEnabled") Boolean notificationEnabled ); - - Optional findMemberByTag(String tag); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java index fda8ce67c..82dd8241b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java @@ -31,13 +31,14 @@ public Binding capsuleSkinFailBinding() { @Bean public Queue groupInviteFailQueue() { - return new Queue(RabbitmqComponentConstants.GROUP_INVITE_QUEUE.getFailComponent(), true); + return new Queue( + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_QUEUE.getFailComponent(), true); } @Bean public DirectExchange groupInviteFailExchange() { return new DirectExchange( - RabbitmqComponentConstants.GROUP_INVITE_EXCHANGE.getFailComponent()); + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_EXCHANGE.getFailComponent()); } @Bean @@ -48,6 +49,26 @@ public Binding groupInviteFailBinding() { .withQueueName(); } + @Bean + public Queue groupAcceptFailQueue() { + return new Queue( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_QUEUE.getFailComponent(), true); + } + + @Bean + public DirectExchange groupAcceptFailExchange() { + return new DirectExchange( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_EXCHANGE.getFailComponent()); + } + + @Bean + public Binding groupAcceptFailBinding() { + return BindingBuilder + .bind(groupAcceptFailQueue()) + .to(groupAcceptFailExchange()) + .withQueueName(); + } + @Bean public Queue friendRequestFailQueue() { return new Queue( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java index cf0b91e0f..4f615029c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java @@ -17,9 +17,15 @@ public enum RabbitmqComponentConstants { "fail.notification.friendAccept.queue"), FRIEND_ACCEPT_NOTIFICATION_EXCHANGE("notification.friendAccept.exchange", "fail.notification.friendAccept.exchange"), - GROUP_INVITE_QUEUE("notification.groupInvite.queue", "fail.notification.groupInvite.queue"), - GROUP_INVITE_EXCHANGE("notification.groupInvite.exchange", - "fail.notification.groupInvite.exchange"); + GROUP_INVITE_NOTIFICATION_QUEUE("notification.groupInvite.queue", + "fail.notification.groupInvite.queue"), + GROUP_INVITE_NOTIFICATION_EXCHANGE("notification.groupInvite.exchange", + "fail.notification.groupInvite.exchange"), + + GROUP_ACCEPT_NOTIFICATION_QUEUE("notification.groupAccept.queue", + "fail.notification.groupAccept.queue"), + GROUP_ACCEPT_NOTIFICATION_EXCHANGE("notification.groupAccept.exchange", + "fail.notification.groupAccept.exchange"); private final String successComponent; private final String failComponent; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java index 3b9763aff..8343c1f3a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java @@ -43,13 +43,14 @@ public Binding capsuleSkinBinding() { @Bean public Queue groupInviteQueue() { - return new Queue(RabbitmqComponentConstants.GROUP_INVITE_QUEUE.getSuccessComponent(), true); + return new Queue( + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_QUEUE.getSuccessComponent(), true); } @Bean public DirectExchange groupInviteExchange() { return new DirectExchange( - RabbitmqComponentConstants.GROUP_INVITE_EXCHANGE.getSuccessComponent()); + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_EXCHANGE.getSuccessComponent()); } @Bean @@ -60,6 +61,26 @@ public Binding groupInviteBinding() { .withQueueName(); } + @Bean + public Queue groupAcceptQueue() { + return new Queue( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_QUEUE.getSuccessComponent(), true); + } + + @Bean + public DirectExchange groupAcceptExchange() { + return new DirectExchange( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_EXCHANGE.getSuccessComponent()); + } + + @Bean + public Binding groupAcceptBinding() { + return BindingBuilder + .bind(groupAcceptQueue()) + .to(groupAcceptExchange()) + .withQueueName(); + } + @Bean public Queue friendRequestQueue() { return new Queue( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java index 29a741814..929fc369d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java @@ -63,6 +63,8 @@ public enum ErrorCode { //group GROUP_CREATE_ERROR(400, "GROUP-001", "그룹 생성에 실패하였습니다."), GROUP_NOT_FOUND_ERROR(404, "GROUP-002", "그룹을 찾을 수 없습니다"), + GROUP_OWNER_AUTHENTICATE_ERROR(400, "GROUP-003", "그룹장이 아닙니다."), + GROUP_INVITATION_NOT_FOUND_ERROR(404, "GROUP-004", "그룹 초대를 찾을 수 없습니다"), //friend invite FRIEND_INVITE_NOT_FOUND_ERROR(404, "FRIEND-INVITE-001", "친구 요청을 찾지 못하였습니다."), diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/GroupAcceptNotificationDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/GroupAcceptNotificationDto.java new file mode 100644 index 000000000..cb4e24be3 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/GroupAcceptNotificationDto.java @@ -0,0 +1,27 @@ +package site.timecapsulearchive.core.infra.queue.data.dto; + +import lombok.Builder; +import site.timecapsulearchive.core.domain.member.entity.NotificationStatus; + +@Builder +public record GroupAcceptNotificationDto( + Long targetId, + NotificationStatus notificationStatus, + String title, + String text +) { + + public static GroupAcceptNotificationDto createOf( + final Long targetId, + final String groupMemberNickname + ) { + final NotificationRequestMessage groupAcceptRequest = NotificationRequestMessage.GROUP_ACCEPT; + + return new GroupAcceptNotificationDto( + targetId, + groupAcceptRequest.getStatus(), + groupAcceptRequest.getTitle(), + groupAcceptRequest.buildPrefixText(groupMemberNickname) + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java index 5cffeba8e..c0bdc06b4 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java @@ -6,7 +6,8 @@ public enum NotificationRequestMessage { CAPSULE_SKIN(NotificationStatus.SUCCESS, "캡슐 스킨 생성 알림", "이 생성되었습니다."), FRIEND_REQUEST(NotificationStatus.SUCCESS, "친구 요청 알림", "님 으로부터 친구 요청이 왔습니다."), FRIEND_ACCEPT(NotificationStatus.SUCCESS, "친구 수락 알림", "님이 친구 요청을 수락하였습니다."), - GROUP_INVITE(NotificationStatus.SUCCESS, "그룹 초대 알림", "님으로부터 그룹 초대 요청이 왔습니다."); + GROUP_INVITE(NotificationStatus.SUCCESS, "그룹 초대 알림", "님으로부터 그룹 초대 요청이 왔습니다."), + GROUP_ACCEPT(NotificationStatus.SUCCESS, "그룹 수락 알림", "님이 그룹 요청을 수락하였습니다."); private final NotificationStatus status; private final String title; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java index b41e4da39..888748a02 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java @@ -8,6 +8,7 @@ import site.timecapsulearchive.core.infra.queue.data.dto.FriendAcceptNotificationDto; import site.timecapsulearchive.core.infra.queue.data.dto.FriendReqNotificationDto; import site.timecapsulearchive.core.infra.queue.data.dto.FriendsReqNotificationsDto; +import site.timecapsulearchive.core.infra.queue.data.dto.GroupAcceptNotificationDto; import site.timecapsulearchive.core.infra.queue.data.dto.GroupInviteNotificationDto; @Component @@ -71,9 +72,20 @@ public void sendGroupInviteMessage( final List targetIds ) { basicRabbitTemplate.convertAndSend( - RabbitmqComponentConstants.GROUP_INVITE_EXCHANGE.getSuccessComponent(), - RabbitmqComponentConstants.GROUP_INVITE_QUEUE.getSuccessComponent(), + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_EXCHANGE.getSuccessComponent(), + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_QUEUE.getSuccessComponent(), GroupInviteNotificationDto.createOf(ownerNickname, groupProfileUrl, targetIds) ); } + + public void sendGroupAcceptMessage( + final String groupMemberNickname, + final Long targetId + ) { + basicRabbitTemplate.convertAndSend( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_EXCHANGE.getSuccessComponent(), + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_QUEUE.getSuccessComponent(), + GroupAcceptNotificationDto.createOf(targetId, groupMemberNickname) + ); + } } diff --git a/backend/core/src/main/resources/db/migration/V25__group_invite_update.sql b/backend/core/src/main/resources/db/migration/V25__group_invite_update.sql new file mode 100644 index 000000000..729e12a70 --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V25__group_invite_update.sql @@ -0,0 +1,20 @@ +alter table group_invite + drop foreign key fk_group_invite_member_id; + +alter table group_invite + drop column member_id; + + +ALTER TABLE group_invite + ADD COLUMN group_owner_id BIGINT; +ALTER TABLE group_invite + ADD COLUMN group_member_id BIGINT; + +ALTER TABLE group_invite + ADD CONSTRAINT fk_group_invite_group_owner_id FOREIGN KEY (group_owner_id) REFERENCES member (member_id); + +ALTER TABLE group_invite + ADD CONSTRAINT fk_group_invite_group_member_id FOREIGN KEY (group_member_id) REFERENCES member (member_id); + +ALTER TABLE group_invite + ADD CONSTRAINT unique_owner_member_pair UNIQUE (group_owner_id, group_member_id); diff --git a/backend/core/src/main/resources/db/migration/V26__group_invite_add.sql b/backend/core/src/main/resources/db/migration/V26__group_invite_add.sql new file mode 100644 index 000000000..61254b875 --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V26__group_invite_add.sql @@ -0,0 +1,3 @@ +alter table group_invite add column group_id BIGINT; +ALTER TABLE group_invite + ADD CONSTRAINT fk_group_invite_group_id FOREIGN KEY (group_id) REFERENCES `group` (group_id); \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/dependency/TestTransactionTemplate.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/dependency/TestTransactionTemplate.java new file mode 100644 index 000000000..36a7970d4 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/dependency/TestTransactionTemplate.java @@ -0,0 +1,21 @@ +package site.timecapsulearchive.core.common.dependency; + +import static org.mockito.Mockito.spy; + +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.SimpleTransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class TestTransactionTemplate extends TransactionTemplate { + + public static TestTransactionTemplate spied() { + return spy(new TestTransactionTemplate()); + } + + @Override + public T execute(TransactionCallback action) throws TransactionException { + return action.doInTransaction(new SimpleTransactionStatus()); + } + +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/GroupDtoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/GroupDtoFixture.java new file mode 100644 index 000000000..ca4196785 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/GroupDtoFixture.java @@ -0,0 +1,23 @@ +package site.timecapsulearchive.core.common.fixture.dto; + +import java.util.List; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; + +public class GroupDtoFixture { + + public static GroupCreateDto groupCreateDto(List targetIds) { + return GroupCreateDto.builder() + .groupName("testGroupName") + .groupProfileUrl("testGroupProfileUrl") + .groupImage("testGroupImage") + .description("testDescription") + .targetIds(targetIds) + .build(); + } + + public static GroupOwnerSummaryDto groupOwnerSummaryDto(Boolean isOwner) { + return new GroupOwnerSummaryDto("testNickname", isOwner, "testGroupProfileUrl"); + } + +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java index bb5f408de..08f0234d4 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java @@ -26,6 +26,8 @@ import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupQueryRepository; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupQueryRepositoryImpl; import site.timecapsulearchive.core.domain.member.entity.Member; @TestConstructor(autowireMode = AutowireMode.ALL) @@ -40,7 +42,7 @@ class MemberGroupQueryRepositoryTest extends RepositoryTest { private Long ownerGroupId; MemberGroupQueryRepositoryTest(JPAQueryFactory jpaQueryFactory) { - this.groupQueryRepository = new GroupQueryRepository(jpaQueryFactory); + this.groupQueryRepository = new GroupQueryRepositoryImpl(jpaQueryFactory); } @Transactional diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupWriteServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupWriteServiceTest.java new file mode 100644 index 000000000..e3b158f37 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupWriteServiceTest.java @@ -0,0 +1,233 @@ +package site.timecapsulearchive.core.domain.group.service; + + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.common.dependency.TestTransactionTemplate; +import site.timecapsulearchive.core.common.fixture.domain.GroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.dto.GroupDtoFixture; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; +import site.timecapsulearchive.core.domain.group.exception.GroupInviteNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupOwnerAuthenticateException; +import site.timecapsulearchive.core.domain.group.repository.groupInviteRepository.GroupInviteRepository; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupRepository; +import site.timecapsulearchive.core.domain.group.repository.memberGroupRepository.MemberGroupRepository; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteService; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteServiceImpl; +import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; +import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; + +class GroupWriteServiceTest { + + private final MemberRepository memberRepository = mock(MemberRepository.class); + private final GroupRepository groupRepository = mock(GroupRepository.class); + private final MemberGroupRepository memberGroupRepository = mock(MemberGroupRepository.class); + private final GroupInviteRepository groupInviteRepository = mock(GroupInviteRepository.class); + private final SocialNotificationManager socialNotificationManager = mock( + SocialNotificationManager.class); + private final TransactionTemplate transactionTemplate = TestTransactionTemplate.spied(); + + private final GroupWriteService groupWriteService = new GroupWriteServiceImpl( + memberRepository, + groupRepository, + memberGroupRepository, + groupInviteRepository, + transactionTemplate, + socialNotificationManager + ); + + @Test + void 그룹장이_그룹원들을_포함하여_그룹을_생성하면_그룹원들에게_그룹초대_알림이_요청된다() { + //given + Long memberId = 1L; + List targetIds = List.of(2L, 3L, 4L, 5L); + GroupCreateDto dto = GroupDtoFixture.groupCreateDto(targetIds); + given(memberRepository.findMemberById(memberId)).willReturn( + Optional.of(MemberFixture.member(1))); + + //when + groupWriteService.createGroup(memberId, dto); + + //then + verify(socialNotificationManager, times(1)).sendGroupInviteMessage( + anyString(), anyString(), anyList()); + } + + @Test + void 그룹장이_그룹초대를_할_때_존재하지_않는_그룹장_아이디면_예외가_발생한다() { + //given + Long memberId = -1L; + List targetIds = List.of(2L, 3L, 4L, 5L); + GroupCreateDto dto = GroupDtoFixture.groupCreateDto(targetIds); + given(memberRepository.findMemberById(memberId)).willReturn(Optional.empty()); + + //when + //then + assertThatThrownBy(() -> groupWriteService.createGroup(memberId, dto)) + .isInstanceOf(MemberNotFoundException.class) + .hasMessageContaining(ErrorCode.MEMBER_NOT_FOUND_ERROR.getMessage()); + } + + + @Test + void 그룹장이_그룹원에게_그룹초대를_하면_그룹초대_알림이_요청된다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupOwner = MemberFixture.member(1); + GroupOwnerSummaryDto groupOwnerSummaryDto = GroupDtoFixture.groupOwnerSummaryDto(true); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupOwner)); + given(memberRepository.findMemberById(targetId)).willReturn( + Optional.of(MemberFixture.member(2))); + given(memberGroupRepository.findOwnerInMemberGroup(groupId, memberId)).willReturn( + Optional.of(groupOwnerSummaryDto)); + + //when + groupWriteService.inviteGroup(memberId, groupId, targetId); + + //then + verify(socialNotificationManager, times(1)).sendGroupInviteMessage(anyString(), anyString(), + anyList()); + } + + @Test + void 그룹장이_그룹초대를_할_때_존재하지_않은_그룹_아이디_이면_예외가_발생한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupOwner = MemberFixture.member(1); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupOwner)); + given(memberRepository.findMemberById(targetId)).willReturn( + Optional.of(MemberFixture.member(2))); + given(memberGroupRepository.findOwnerInMemberGroup(groupId, memberId)).willReturn( + Optional.empty()); + + //when + //then + assertThatThrownBy(() -> groupWriteService.inviteGroup(memberId, groupId, targetId)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_NOT_FOUND_ERROR.getMessage()); + } + + @Test + void 그룹장이_아닌_사용자가_그룹원에게_그룹초대를_하면_예외가_발생한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupOwner = MemberFixture.member(1); + GroupOwnerSummaryDto groupOwnerSummaryDto = GroupDtoFixture.groupOwnerSummaryDto(false); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupOwner)); + given(memberRepository.findMemberById(targetId)).willReturn( + Optional.of(MemberFixture.member(2))); + given(memberGroupRepository.findOwnerInMemberGroup(groupId, memberId)).willReturn( + Optional.of(groupOwnerSummaryDto)); + + //when + //then + assertThatThrownBy(() -> groupWriteService.inviteGroup(memberId, groupId, targetId)) + .isInstanceOf(GroupOwnerAuthenticateException.class) + .hasMessageContaining(ErrorCode.GROUP_OWNER_AUTHENTICATE_ERROR.getMessage()); + } + + @Test + void 그룹원은_그룹초대_삭제에서_1을_반환하면_거부할_수_있다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(1); + + //when + // then + assertThatCode(() -> groupWriteService.rejectRequestGroup(memberId, groupId, targetId)) + .doesNotThrowAnyException(); + } + + @Test + void 그룹원은_그룹초대_삭제에서_0을_반환하면_거부가_실패_한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(0); + + //when + // then + assertThatThrownBy(() -> groupWriteService.rejectRequestGroup(memberId, groupId, targetId)) + .isInstanceOf(GroupInviteNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_INVITATION_NOT_FOUND_ERROR.getMessage()); + } + + @Test + void 그룹원은_그룹초대를_수락하면_그룹장에게_알림이_전송된다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupMember = MemberFixture.member(1); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupMember)); + given(groupRepository.findGroupById(groupId)).willReturn( + Optional.of(GroupFixture.group())); + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(1); + + //when + groupWriteService.acceptGroupInvite(memberId, groupId, targetId); + + //then + verify(socialNotificationManager, times(1)).sendGroupAcceptMessage(anyString(), anyLong()); + } + + @Test + void 그룹원은_그룹초대를_수락할_때_그룹초대가_존재하지_않으면_예외가_발생한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupMember = MemberFixture.member(1); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupMember)); + given(groupRepository.findGroupById(groupId)).willReturn( + Optional.of(GroupFixture.group())); + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(0); + + //when + //then + assertThatThrownBy(() -> groupWriteService.acceptGroupInvite(memberId, groupId, targetId)) + .isInstanceOf(GroupInviteNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_INVITATION_NOT_FOUND_ERROR.getMessage()); + } + + +} + diff --git a/backend/notification/src/main/resources/config b/backend/notification/src/main/resources/config index 9ba78f7fb..8d3af3cc4 160000 --- a/backend/notification/src/main/resources/config +++ b/backend/notification/src/main/resources/config @@ -1 +1 @@ -Subproject commit 9ba78f7fb830e195236ce34d400b5a9535683e35 +Subproject commit 8d3af3cc4263c89e6f6421770e64bc9083a7ad6d diff --git a/frontend/ARchive/app/build.gradle b/frontend/ARchive/app/build.gradle index c0bc7028d..fb654f877 100644 --- a/frontend/ARchive/app/build.gradle +++ b/frontend/ARchive/app/build.gradle @@ -6,6 +6,7 @@ plugins { id 'dagger.hilt.android.plugin' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' + id 'com.google.android.gms.oss-licenses-plugin' } Properties properties = new Properties() @@ -141,7 +142,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:20.7.0' //네이버 지도 : https://navermaps.github.io/android-map-sdk/guide-ko/1.html - implementation 'com.naver.maps:map-sdk:3.17.0' + implementation 'com.naver.maps:map-sdk:3.18.0' // Jetpack Security implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06' @@ -178,6 +179,16 @@ dependencies { // Event Bus : https://github.com/greenrobot/EventBus implementation("org.greenrobot:eventbus:3.3.1") + + // oss-licenses : https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin + implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1' + + // Swiperefreshlayout: https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout?hl=ko#kts + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + + // ConcatAdapter + implementation "androidx.recyclerview:recyclerview:1.3.2" + } kapt { correctErrorTypes true diff --git a/frontend/ARchive/app/src/main/AndroidManifest.xml b/frontend/ARchive/app/src/main/AndroidManifest.xml index 0522b7c76..ec263a56d 100644 --- a/frontend/ARchive/app/src/main/AndroidManifest.xml +++ b/frontend/ARchive/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + @@ -24,16 +25,41 @@ android:allowBackup="false" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="false" - android:icon="@mipmap/ic_launcher" + android:icon="@drawable/app_symbol" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" + android:roundIcon="@drawable/app_symbol" android:screenOrientation="portrait" android:supportsRtl="true" android:theme="@style/Theme.ARchive" android:usesCleartextTraffic="true" tools:targetApi="31"> + + + + + + - + tools:ignore="LockedOrientationActivity" /> + + - - + @@ -138,7 +172,6 @@ - \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule/response/NearbyCapsuleResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule/response/NearbyCapsuleResponseDto.kt index a6a692807..a63caccd8 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule/response/NearbyCapsuleResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule/response/NearbyCapsuleResponseDto.kt @@ -1,12 +1,16 @@ package com.droidblossom.archive.data.dto.capsule.response import com.droidblossom.archive.data.dto.common.CapsuleSummaryDto -import com.droidblossom.archive.domain.model.capsule.NearbyCapsule +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchors +import com.droidblossom.archive.domain.model.capsule.CapsuleMarkers data class NearbyCapsuleResponseDto ( val capsules : List ){ - fun toModel() = NearbyCapsule( - capsules = this.capsules.map { it.toModel() } + fun toMarkerModel() = CapsuleMarkers( + capsuleMarkers = this.capsules.map { it.toMarkerModel() } + ) + fun toAnchorModel() = CapsuleAnchors( + capsuleAnchors = this.capsules.map { it.toAnchorModel() } ) } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule_skin/request/CapsuleSkinsPageRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule_skin/request/CapsuleSkinsPageRequestDto.kt deleted file mode 100644 index e33b9f8fc..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/capsule_skin/request/CapsuleSkinsPageRequestDto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.droidblossom.archive.data.dto.capsule_skin.request - -data class CapsuleSkinsPageRequestDto ( - val size : Int, - val createdAt: String -) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/request/SecretCapsuleCreateRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleCreateRequestDto.kt similarity index 69% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/request/SecretCapsuleCreateRequestDto.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleCreateRequestDto.kt index 7edd1bc6e..d19dd010a 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/request/SecretCapsuleCreateRequestDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleCreateRequestDto.kt @@ -1,9 +1,8 @@ -package com.droidblossom.archive.data.dto.secret.request +package com.droidblossom.archive.data.dto.common import com.droidblossom.archive.data.dto.capsule.response.AddressDataDto -import com.droidblossom.archive.data.dto.common.FileNameDto -data class SecretCapsuleCreateRequestDto( +data class CapsuleCreateRequestDto( val capsuleSkinId: Long, val content: String, val directory: String, diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleDetailResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleDetailResponseDto.kt similarity index 67% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleDetailResponseDto.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleDetailResponseDto.kt index 579ee9263..a9ae77315 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleDetailResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleDetailResponseDto.kt @@ -1,12 +1,16 @@ -package com.droidblossom.archive.data.dto.secret.response +package com.droidblossom.archive.data.dto.common -import com.droidblossom.archive.domain.model.secret.SecretCapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleDetail +import com.droidblossom.archive.domain.model.common.SocialCapsules -data class SecretCapsuleDetailResponseDto( +data class CapsuleDetailResponseDto( val address: String, + val roadName: String?, val capsuleSkinUrl: String, val content: String?, val createdDate: String, + val latitude: Double, + val longitude: Double, val profileUrl: String, val dueDate: String?, val isOpened: Boolean, @@ -16,8 +20,9 @@ data class SecretCapsuleDetailResponseDto( val title: String, val capsuleType: String, ){ - fun toModel() = SecretCapsuleDetail( + fun toModel() = CapsuleDetail( address = this.address, + roadName = this.roadName ?: "", capsuleSkinUrl = this.capsuleSkinUrl, content = this.content ?: "", createdDate = this.createdDate, diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryDto.kt index 3b20356c7..dbff81d36 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryDto.kt @@ -1,6 +1,7 @@ package com.droidblossom.archive.data.dto.common -import com.droidblossom.archive.domain.model.common.CapsuleMarker +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchor +import com.droidblossom.archive.domain.model.capsule.CapsuleMarker import com.droidblossom.archive.presentation.ui.home.HomeFragment data class CapsuleSummaryDto( @@ -13,11 +14,18 @@ data class CapsuleSummaryDto( val dueDate: String, val capsuleType: String, ){ - fun toModel() = CapsuleMarker( + fun toAnchorModel() = CapsuleAnchor( id = this.id, longitude = this.longitude, latitude = this.latitude, capsuleType = HomeFragment.CapsuleType.valueOf(capsuleType), skinUrl = this.capsuleSkinUrl ) + + fun toMarkerModel() = CapsuleMarker( + id = this.id, + longitude = this.longitude, + latitude = this.latitude, + capsuleType = HomeFragment.CapsuleType.valueOf(capsuleType), + ) } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleSummaryResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryResponseDto.kt similarity index 60% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleSummaryResponseDto.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryResponseDto.kt index 49347289e..2bc764e3d 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleSummaryResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/CapsuleSummaryResponseDto.kt @@ -1,26 +1,28 @@ -package com.droidblossom.archive.data.dto.secret.response +package com.droidblossom.archive.data.dto.common -import com.droidblossom.archive.domain.model.secret.SecretCapsuleSummary +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse -data class SecretCapsuleSummaryResponseDto( +data class CapsuleSummaryResponseDto( val nickname: String, val profileUrl: String, val skinUrl: String, val title: String, val dueDate: String?, + val latitude: Double, + val longitude: Double, val address: String, - val roadName: String, + val roadName: String?, val isOpened: Boolean, val createdAt: String ){ - fun toModel() = SecretCapsuleSummary( + fun toModel() = CapsuleSummaryResponse( nickname = this.nickname, profileUrl = this.profileUrl, skinUrl = this.skinUrl, title = this.title, dueDate = this.dueDate ?: "", address = this.address, - roadName = this.roadName, + roadName = this.roadName ?: "", isOpened = this.isOpened, createdAt = this.createdAt, ) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/PagingRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/PagingRequestDto.kt new file mode 100644 index 000000000..d28d9a90f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/common/PagingRequestDto.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.data.dto.common + +data class PagingRequestDto( + val size : Int, + val createdAt: String +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendAcceptRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendAcceptRequestDto.kt new file mode 100644 index 000000000..a3de310a6 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendAcceptRequestDto.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.data.dto.friend.request + +data class FriendAcceptRequestDto ( + val friendId : Long +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendReqRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendReqRequestDto.kt new file mode 100644 index 000000000..6b8fc0669 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendReqRequestDto.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.data.dto.friend.request + +data class FriendReqRequestDto( + val friendId : Long +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsReqRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsReqRequestDto.kt new file mode 100644 index 000000000..a73861f8e --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsReqRequestDto.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.data.dto.friend.request + +data class FriendsReqRequestDto( + val friendIds : List +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsSearchPhoneRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsSearchPhoneRequestDto.kt new file mode 100644 index 000000000..4164058a6 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsSearchPhoneRequestDto.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.data.dto.friend.request + +data class FriendsSearchPhoneRequestDto( + val phoneBooks : List +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsSearchRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsSearchRequestDto.kt new file mode 100644 index 000000000..31e865c2b --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/FriendsSearchRequestDto.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.data.dto.friend.request + +data class FriendsSearchRequestDto ( + val friendTag : String +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/PhoneBooks.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/PhoneBooks.kt new file mode 100644 index 000000000..ce9ed29c3 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/request/PhoneBooks.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.data.dto.friend.request + +data class PhoneBooks ( + val originPhone: String, + val originName : String, +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendReqStatusResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendReqStatusResponseDto.kt new file mode 100644 index 000000000..58272de4a --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendReqStatusResponseDto.kt @@ -0,0 +1,15 @@ +package com.droidblossom.archive.data.dto.friend.response + +import com.droidblossom.archive.domain.model.friend.FriendReqStatusResponse +import java.io.Serializable + +data class FriendReqStatusResponseDto ( + val httpStatus : String, + val result : String +) : Serializable { + + fun toModel() = FriendReqStatusResponse( + httpStatus = this.httpStatus, + result = this.result, + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendResponseDto.kt new file mode 100644 index 000000000..729a3c519 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendResponseDto.kt @@ -0,0 +1,18 @@ +package com.droidblossom.archive.data.dto.friend.response + +import com.droidblossom.archive.domain.model.friend.Friend + +data class FriendResponseDto( + val createdAt: String, + val id: Long, + val nickname: String, + val profileUrl: String +) { + fun toModel() = Friend( + createdAt = this.createdAt, + id = this.id, + nickname = this.nickname, + profileUrl = this.profileUrl, + isOpenDelete = false + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsPageResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsPageResponseDto.kt new file mode 100644 index 000000000..51ccca46f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsPageResponseDto.kt @@ -0,0 +1,13 @@ +package com.droidblossom.archive.data.dto.friend.response + +import com.droidblossom.archive.domain.model.friend.FriendsPage + +data class FriendsPageResponseDto( + val friends: List, + val hasNext: Boolean +) { + fun toModel() = FriendsPage( + friends = this.friends.map { it.toModel() }, + hasNext = this.hasNext + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsSearchPhoneResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsSearchPhoneResponseDto.kt new file mode 100644 index 000000000..54534b438 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsSearchPhoneResponseDto.kt @@ -0,0 +1,13 @@ +package com.droidblossom.archive.data.dto.friend.response + +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneResponse +import java.io.Serializable + +data class FriendsSearchPhoneResponseDto( + val friends : List +) : Serializable { + + fun toModel() = FriendsSearchPhoneResponse( + friends = this.friends.map { it.toModel() }, + ) +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsSearchResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsSearchResponseDto.kt new file mode 100644 index 000000000..ba823d1e2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/friend/response/FriendsSearchResponseDto.kt @@ -0,0 +1,27 @@ +package com.droidblossom.archive.data.dto.friend.response + +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse +import java.io.Serializable + +data class FriendsSearchResponseDto( + val id: Long, + val profileUrl: String, + val originName : String?, + val nickname: String, + val isFriend: Boolean, + val isFriendInviteToFriend : Boolean, + val isFriendInviteToMe : Boolean, + val isFriendRequest :Boolean, +) : Serializable { + + fun toModel() = FriendsSearchResponse( + id = this.id, + profileUrl = this.profileUrl, + nickname = this.nickname, + isFriend = this.isFriend, + isFriendInviteToFriend = this.isFriendInviteToFriend, + isFriendInviteToMe = this.isFriendInviteToMe, + name = this.originName ?: "", + isChecked = false + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/MemberDetailResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/MemberDetailResponseDto.kt index bf2424dde..28d431471 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/MemberDetailResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/MemberDetailResponseDto.kt @@ -6,12 +6,16 @@ import java.io.Serializable data class MemberDetailResponseDto( val nickname : String, val profileUrl : String, - val phone : String + val tag : String, + val friendCount: Int, + val groupCount: Int ) : Serializable { fun toModel() = MemberDetail( nickname = this.nickname, profileUrl = this.profileUrl, - phone = this.phone + tag = this.tag, + friendCount = this.friendCount, + groupCount = this.groupCount, ) } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/NoficationListResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/NoficationListResponseDto.kt index ce49c8d5c..f052f7b7a 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/NoficationListResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/member/response/NoficationListResponseDto.kt @@ -1,17 +1,22 @@ package com.droidblossom.archive.data.dto.member.response +import com.droidblossom.archive.domain.model.member.NotiCategoryName import com.droidblossom.archive.domain.model.member.NotificationModel data class NoficationListResponseDto( val createdAt: String, val imageUrl: String, val text: String, - val title: String + val title: String, + val categoryName: String, + val status: String? ) { fun toModel() = NotificationModel( createdAt = this.createdAt, imageUrl = this.imageUrl, text = this.text, - title = this.title + title = this.title, + categoryName = NotiCategoryName.valueOf(categoryName), + status = this.status ?: "", ) } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/MyPublicCapsulePageResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/MyPublicCapsulePageResponseDto.kt new file mode 100644 index 000000000..3effed842 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/MyPublicCapsulePageResponseDto.kt @@ -0,0 +1,15 @@ +package com.droidblossom.archive.data.dto.open.response + +import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleResponseDto +import com.droidblossom.archive.domain.model.secret.CapsulePageList + +data class MyPublicCapsulePageResponseDto ( + val publicCapsules: List, + val hasNext: Boolean, +){ + fun toModel() = CapsulePageList( + capsules = this.publicCapsules.map { it.toUIModel() }, + hasNext = this.hasNext, + hasPrevious = this.hasNext, + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/MyPublicCapsuleResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/MyPublicCapsuleResponseDto.kt new file mode 100644 index 000000000..417457992 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/MyPublicCapsuleResponseDto.kt @@ -0,0 +1,23 @@ +package com.droidblossom.archive.data.dto.open.response + +import com.droidblossom.archive.presentation.model.mypage.CapsuleData + +data class MyPublicCapsuleResponseDto( + val capsuleId : Long, + val skinUrl : String, + val dueDate : String?, + val createdAt : String, + val title : String, + val isOpened : Boolean, + val capsuleType : String, +){ + fun toUIModel() = CapsuleData( + capsuleId = this.capsuleId, + capsuleSkinUrl = this.skinUrl, + createdDate = this.createdAt, + dueDate = this.dueDate, + isOpened = this.isOpened, + title = this.title, + capsuleType = this.capsuleType + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/PublicCapsuleResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/PublicCapsuleResponseDto.kt new file mode 100644 index 000000000..168c2e184 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/PublicCapsuleResponseDto.kt @@ -0,0 +1,38 @@ +package com.droidblossom.archive.data.dto.open.response + +import com.droidblossom.archive.data.dto.common.CapsuleDetailResponseDto +import com.droidblossom.archive.domain.model.common.SocialCapsules +import com.droidblossom.archive.domain.model.open.PublicCapsuleSliceResponse + +data class PublicCapsuleResponseDto ( + val capsuleId: Long, + val address: String, + val roadName: String, + val capsuleSkinUrl: String, + val content: String?, + val createdDate: String, + val profileUrl: String, + val dueDate: String?, + val isOpened: Boolean, + val imageUrls: List?, + val videoUrls: List?, + val nickname: String, + val title: String, + val capsuleType: String, +){ + fun toModel()= SocialCapsules( + capsuleId = this.capsuleId, + title = this.title, + content = this.content ?: "", + createdDate = this.createdDate, + dueDate = this.dueDate, + profileUrl = this.profileUrl, + capsuleSkinUrl = this.capsuleSkinUrl, + nickNameOrGroupName = this.nickname, + roadName = this.roadName, + address = this.address, + hasThumbnail = !(imageUrls.isNullOrEmpty() && videoUrls.isNullOrEmpty()), + thumbnailImage = imageUrls?.firstOrNull() ?: videoUrls?.firstOrNull() ?: "", + isOpened = this.isOpened + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/PublicCapsuleSliceResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/PublicCapsuleSliceResponseDto.kt new file mode 100644 index 000000000..a34bca078 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/open/response/PublicCapsuleSliceResponseDto.kt @@ -0,0 +1,15 @@ +package com.droidblossom.archive.data.dto.open.response + +import com.droidblossom.archive.data.dto.common.CapsuleDetailResponseDto +import com.droidblossom.archive.domain.model.open.PublicCapsuleSliceResponse +import com.droidblossom.archive.domain.model.s3.S3Urls + +data class PublicCapsuleSliceResponseDto( + val publicCapsules: List, + val hasNext: Boolean +){ + fun toModel()= PublicCapsuleSliceResponse( + publicCapsules = this.publicCapsules.map { it.toModel() }, + hasNext = this.hasNext + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/request/SecretCapsulePageRequestDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/request/SecretCapsulePageRequestDto.kt deleted file mode 100644 index 6c55d8f1a..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/request/SecretCapsulePageRequestDto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.droidblossom.archive.data.dto.secret.request - -data class SecretCapsulePageRequestDto( - val size : Int, - val createdAt: String -) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsulePageResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsulePageResponseDto.kt index 2979aca4c..90ab7c282 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsulePageResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsulePageResponseDto.kt @@ -1,14 +1,14 @@ package com.droidblossom.archive.data.dto.secret.response -import com.droidblossom.archive.domain.model.secret.SecretCapsulePage +import com.droidblossom.archive.domain.model.secret.CapsulePageList data class SecretCapsulePageResponseDto( val capsules: List, val hasNext: Boolean, val hasPrevious: Boolean ){ - fun toModel() = SecretCapsulePage( - capsules = this.capsules.map { it.toModel() }, + fun toModel() = CapsulePageList( + capsules = this.capsules.map { it.toUIModel() }, hasNext = this.hasNext, hasPrevious = this.hasPrevious, ) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleResponseDto.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleResponseDto.kt index f190fdbdf..a7236901e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleResponseDto.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/dto/secret/response/SecretCapsuleResponseDto.kt @@ -1,6 +1,7 @@ package com.droidblossom.archive.data.dto.secret.response import com.droidblossom.archive.domain.model.common.MyCapsule +import com.droidblossom.archive.presentation.model.mypage.CapsuleData data class SecretCapsuleResponseDto( val capsuleId: Long, @@ -10,14 +11,24 @@ data class SecretCapsuleResponseDto( val isOpened: Boolean, val title: String, val type: String, -){ - fun toModel()=MyCapsule( +) { + fun toModel() = MyCapsule( capsuleId = this.capsuleId, capsuleSkinUrl = this.SkinUrl, createdDate = this.createdAt, dueDate = this.dueDate, isOpened = this.isOpened, - title =this.title, + title = this.title, + capsuleType = this.type + ) + + fun toUIModel() = CapsuleData( + capsuleId = this.capsuleId, + capsuleSkinUrl = this.SkinUrl, + createdDate = this.createdAt, + dueDate = this.dueDate, + isOpened = this.isOpened, + title = this.title, capsuleType = this.type ) } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleRepositoryImpl.kt index 8509098f7..ddb5f753f 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleRepositoryImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleRepositoryImpl.kt @@ -5,11 +5,11 @@ import com.droidblossom.archive.data.dto.capsule.response.AddressDataDto import com.droidblossom.archive.data.dto.capsule.response.CapsuleImagesDto import com.droidblossom.archive.data.dto.capsule.response.CapsuleOpenedResponseDto import com.droidblossom.archive.data.dto.capsule.response.NearbyCapsuleResponseDto -import com.droidblossom.archive.data.dto.common.toModel import com.droidblossom.archive.data.source.remote.api.CapsuleService import com.droidblossom.archive.domain.model.capsule.CapsuleImages import com.droidblossom.archive.domain.model.capsule.CapsuleOpenedResponse -import com.droidblossom.archive.domain.model.capsule.NearbyCapsule +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchors +import com.droidblossom.archive.domain.model.capsule.CapsuleMarkers import com.droidblossom.archive.domain.model.common.AddressData import com.droidblossom.archive.domain.repository.CapsuleRepository import com.droidblossom.archive.util.RetrofitResult @@ -23,13 +23,39 @@ class CapsuleRepositoryImpl @Inject constructor( return apiHandler({ api.patchCapsuleOpenApi(capsuleId = capsuleId) }) { response: ResponseBody -> response.result.toModel() } } - override suspend fun NearbyCapsules( + override suspend fun nearbyMyCapsulesHome( latitude: Double, longitude: Double, distance: Double, - capsule_type: String - ): RetrofitResult { - return apiHandler({ api.getNearbyCapsulesApi(latitude = latitude, longitude = longitude, distance= distance, capsule_type= capsule_type) }) { response : ResponseBody -> response.result.toModel() } + capsuleType: String + ): RetrofitResult { + return apiHandler({ api.getNearbyMyCapsulesHomeApi(latitude = latitude, longitude = longitude, distance= distance, capsuleType= capsuleType) }) { response : ResponseBody -> response.result.toMarkerModel() } + } + + override suspend fun nearbyFriendsCapsulesHome( + latitude: Double, + longitude: Double, + distance: Double + ): RetrofitResult { + return apiHandler({ api.getNearbyFriendsCapsulesHomeApi(latitude = latitude, longitude = longitude, distance= distance ) }) { response : ResponseBody -> response.result.toMarkerModel() } + + } + + override suspend fun nearbyMyCapsulesAR( + latitude: Double, + longitude: Double, + distance: Double, + capsuleType: String + ): RetrofitResult { + return apiHandler({ api.getNearbyMyCapsulesARApi(latitude = latitude, longitude = longitude, distance= distance, capsuleType= capsuleType) }) { response : ResponseBody -> response.result.toAnchorModel() } + } + + override suspend fun nearbyFriendsCapsulesAR( + latitude: Double, + longitude: Double, + distance: Double, + ): RetrofitResult { + return apiHandler({ api.getNearbyFriendsCapsulesARApi(latitude = latitude, longitude = longitude, distance= distance) }) { response : ResponseBody -> response.result.toAnchorModel() } } override suspend fun getAddress( diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleSkinRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleSkinRepositoryImpl.kt index bc5435f28..ecc7ef752 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleSkinRepositoryImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/CapsuleSkinRepositoryImpl.kt @@ -2,31 +2,26 @@ package com.droidblossom.archive.data.repository import com.droidblossom.archive.data.dto.ResponseBody import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsMakeRequestDto -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsSearchPageRequestDto import com.droidblossom.archive.data.dto.capsule_skin.response.CapsuleSkinsMakeResponseDto import com.droidblossom.archive.data.dto.capsule_skin.response.CapsuleSkinsPageResponseDto import com.droidblossom.archive.data.dto.capsule_skin.response.CapsuleSkinsSearchPageResponseDto -import com.droidblossom.archive.data.dto.common.CapsuleSkinSummaryResponseDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.data.dto.common.toModel import com.droidblossom.archive.data.source.remote.api.CapsuleSkinService import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsMakeResponse import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsPageResponse import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsSearchPageResponse -import com.droidblossom.archive.domain.model.common.CapsuleSkinSummary import com.droidblossom.archive.domain.repository.CapsuleSkinRepository import com.droidblossom.archive.util.RetrofitResult import com.droidblossom.archive.util.apiHandler -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody import javax.inject.Inject class CapsuleSkinRepositoryImpl @Inject constructor( private val api : CapsuleSkinService ) : CapsuleSkinRepository{ - override suspend fun getCapsuleSkinPage(request: CapsuleSkinsPageRequestDto): RetrofitResult { + override suspend fun getCapsuleSkinPage(request: PagingRequestDto): RetrofitResult { return apiHandler({ api.getCapsuleSkinsPageApi(request.size, request.createdAt) }) { response: ResponseBody -> response.result.toModel() } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/FriendRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/FriendRepositoryImpl.kt new file mode 100644 index 000000000..6590753ec --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/FriendRepositoryImpl.kt @@ -0,0 +1,65 @@ +package com.droidblossom.archive.data.repository + +import com.droidblossom.archive.data.dto.ResponseBody +import com.droidblossom.archive.data.dto.capsule_skin.response.CapsuleSkinsPageResponseDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.data.dto.common.toModel +import com.droidblossom.archive.data.dto.friend.request.FriendAcceptRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendReqRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsReqRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchPhoneRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchRequestDto +import com.droidblossom.archive.data.dto.friend.response.FriendReqStatusResponseDto +import com.droidblossom.archive.data.dto.friend.response.FriendsPageResponseDto +import com.droidblossom.archive.data.dto.friend.response.FriendsSearchPhoneResponseDto +import com.droidblossom.archive.data.dto.friend.response.FriendsSearchResponseDto +import com.droidblossom.archive.data.source.remote.api.FriendService +import com.droidblossom.archive.domain.model.friend.FriendReqStatusResponse +import com.droidblossom.archive.domain.model.friend.FriendsPage +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneRequest +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneResponse +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.RetrofitResult +import com.droidblossom.archive.util.apiHandler +import javax.inject.Inject + +class FriendRepositoryImpl @Inject constructor( + private val api : FriendService +) : FriendRepository { + override suspend fun postFriendsRequest(request: FriendReqRequestDto): RetrofitResult { + return apiHandler({ api.postFriendsRequestApi(request.friendId) }) { response: ResponseBody -> response.result.toModel() } + } + + override suspend fun postFriendsListRequest(request: FriendsReqRequestDto): RetrofitResult { + return apiHandler({api.postFriendListRequestsPageApi(request)}) { response : ResponseBody ->response.result.toModel()} + } + + override suspend fun postFriendsAcceptRequest(request: FriendAcceptRequestDto): RetrofitResult { + return apiHandler({ api.postFriendsAcceptRequestApi(request.friendId) }) {response: ResponseBody -> response.result.toModel()} + } + + override suspend fun postFriendsSearch(request: FriendsSearchRequestDto): RetrofitResult { + return apiHandler({ api.postFriendsSearchApi(request.friendTag) }) {response: ResponseBody -> response.result.toModel()} + } + + override suspend fun postFriendsSearchPhone(request: FriendsSearchPhoneRequestDto): RetrofitResult { + return apiHandler({ api.postFriendsSearchPhoneApi(request) }) {response: ResponseBody -> response.result.toModel()} + } + + override suspend fun getFriendsPage(request: PagingRequestDto): RetrofitResult { + return apiHandler({api.getFriendsPageApi(request.size, request.createdAt)}) {response: ResponseBody -> response.result.toModel()} + } + + override suspend fun getFriendsRequestsPage(request: PagingRequestDto): RetrofitResult { + return apiHandler({api.getFriendsRequestsPageApi(request.size, request.createdAt)}) {response: ResponseBody -> response.result.toModel()} + } + + override suspend fun deleteFriend(friendId: Long): RetrofitResult { + return apiHandler({api.deleteFriendApi(friendId)}) {response: ResponseBody -> response.result.toModel()} + } + + override suspend fun deleteFriendDeny(friendId: Long): RetrofitResult { + return apiHandler({api.deleteFriendDenyRequestApi(friendId)}) {response: ResponseBody -> response.result.toModel()} + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/KakaoRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/KakaoRepositoryImpl.kt index b7ffab30f..6f64eafa6 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/KakaoRepositoryImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/KakaoRepositoryImpl.kt @@ -5,15 +5,15 @@ import com.droidblossom.archive.domain.repository.KakaoRepository import javax.inject.Inject import javax.inject.Named -class KakaoRepositoryImpl @Inject constructor( +class KakaoRepositoryImpl @Inject constructor( @Named("kakao") private val api: KakaoService ) : KakaoRepository { override suspend fun getAddress(x: String, y: String): String { - val response = api.getAddressApi(x,y) - return if (response.isSuccessful){ + val response = api.getAddressApi(x, y) + return if (response.isSuccessful) { response.body()?.documents?.first()?.road_address?.road_name ?: "null" - }else{ + } else { "null" } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/MemberRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/MemberRepositoryImpl.kt index 72110142a..9150681d8 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/MemberRepositoryImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/MemberRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.droidblossom.archive.data.repository import com.droidblossom.archive.data.dto.ResponseBody import com.droidblossom.archive.data.dto.auth.response.HealthResponseDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.data.dto.common.toModel import com.droidblossom.archive.data.dto.member.request.FcmTokenRequsetDto import com.droidblossom.archive.data.dto.member.request.MemberStatusRequestDto @@ -39,13 +40,12 @@ class MemberRepositoryImpl @Inject constructor( } override suspend fun getNotifications( - size: Int, - createdAt: String + request: PagingRequestDto ): RetrofitResult { return apiHandler({ api.getNotifications( - size = size, - createdAt = createdAt + size = request.size, + createdAt = request.createdAt ) }) { response: ResponseBody -> response.result.toModel() } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/PublicRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/PublicRepositoryImpl.kt new file mode 100644 index 000000000..d72e6c790 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/PublicRepositoryImpl.kt @@ -0,0 +1,43 @@ +package com.droidblossom.archive.data.repository + +import com.droidblossom.archive.data.dto.ResponseBody +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.CapsuleDetailResponseDto +import com.droidblossom.archive.data.dto.common.CapsuleSummaryResponseDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.data.dto.common.toModel +import com.droidblossom.archive.data.dto.open.response.MyPublicCapsulePageResponseDto +import com.droidblossom.archive.data.dto.open.response.PublicCapsuleSliceResponseDto +import com.droidblossom.archive.data.source.remote.api.PublicService +import com.droidblossom.archive.domain.model.common.CapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse +import com.droidblossom.archive.domain.model.open.PublicCapsuleSliceResponse +import com.droidblossom.archive.domain.model.secret.CapsulePageList +import com.droidblossom.archive.domain.repository.PublicRepository +import com.droidblossom.archive.util.RetrofitResult +import com.droidblossom.archive.util.apiHandler +import javax.inject.Inject + +class PublicRepositoryImpl @Inject constructor( + private val api: PublicService +): PublicRepository { + override suspend fun createPublicCapsule(request: CapsuleCreateRequestDto): RetrofitResult { + return apiHandler({ api.postPublicCapsuleApi(request) }) { response: ResponseBody -> response.result.toModel() } + } + + override suspend fun getPublicCapsuleSummary(capsuleId: Long): RetrofitResult { + return apiHandler({ api.getPublicCapsuleSummaryApi(capsuleId) }) { response: ResponseBody -> response.result.toModel()} + } + + override suspend fun getPublicCapsuleDetail(capsuleId: Long): RetrofitResult { + return apiHandler({ api.getPublicCapsuleDetailApi(capsuleId) }) { response: ResponseBody -> response.result.toModel()} + } + + override suspend fun getPublicCapsulesPage(request: PagingRequestDto): RetrofitResult { + return apiHandler({ api.getPublicCapsulesPageApi(request.size, request.createdAt) }){ response: ResponseBody -> response.result.toModel() } + } + + override suspend fun getMyPublicCapsulesPage(request: PagingRequestDto): RetrofitResult { + return apiHandler({ api.getMyPublicCapsulePageApi(request.size, request.createdAt) }){ response: ResponseBody -> response.result.toModel() } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/SecretRepositoryImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/SecretRepositoryImpl.kt index 36873b06b..db963c533 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/SecretRepositoryImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/repository/SecretRepositoryImpl.kt @@ -2,19 +2,18 @@ package com.droidblossom.archive.data.repository import com.droidblossom.archive.data.dto.ResponseBody import com.droidblossom.archive.data.dto.common.toModel -import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleModifyRequestDto -import com.droidblossom.archive.data.dto.secret.request.SecretCapsulePageRequestDto -import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleCreateResponseDto -import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleDetailResponseDto +import com.droidblossom.archive.data.dto.common.CapsuleDetailResponseDto import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleModifyResponseDto import com.droidblossom.archive.data.dto.secret.response.SecretCapsulePageResponseDto -import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleSummaryResponseDto +import com.droidblossom.archive.data.dto.common.CapsuleSummaryResponseDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.data.source.remote.api.SecretService -import com.droidblossom.archive.domain.model.secret.SecretCapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleDetail import com.droidblossom.archive.domain.model.secret.SecretCapsuleModify -import com.droidblossom.archive.domain.model.secret.SecretCapsulePage -import com.droidblossom.archive.domain.model.secret.SecretCapsuleSummary +import com.droidblossom.archive.domain.model.secret.CapsulePageList +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse import com.droidblossom.archive.domain.repository.SecretRepository import com.droidblossom.archive.util.RetrofitResult import com.droidblossom.archive.util.apiHandler @@ -24,24 +23,24 @@ class SecretRepositoryImpl @Inject constructor( private val api: SecretService ) : SecretRepository { - override suspend fun getSecretCapsulePage(request: SecretCapsulePageRequestDto): RetrofitResult { + override suspend fun getSecretCapsulePage(request: PagingRequestDto): RetrofitResult { return apiHandler({ api.getSecretCapsulePageApi(request.size, request.createdAt) }) { response: ResponseBody -> response.result.toModel() } } - override suspend fun createSecretCapsule(request: SecretCapsuleCreateRequestDto): RetrofitResult { + override suspend fun createSecretCapsule(request: CapsuleCreateRequestDto): RetrofitResult { return apiHandler({ api.postSecretCapsuleApi(request) }) { response: ResponseBody -> response.result.toModel() } } - override suspend fun getSecretCapsuleDetail(capsuleId: Long): RetrofitResult { - return apiHandler({ api.getSecretCapsuleDetailApi(capsuleId) }) { response: ResponseBody -> response.result.toModel() } + override suspend fun getSecretCapsuleDetail(capsuleId: Long): RetrofitResult { + return apiHandler({ api.getSecretCapsuleDetailApi(capsuleId) }) { response: ResponseBody -> response.result.toModel() } } - override suspend fun getSecretCapsuleSummary (capsuleId: Int) : RetrofitResult { - return apiHandler({ api.getSecretCapsuleSummaryApi(capsuleId) }) { response: ResponseBody -> response.result.toModel()} + override suspend fun getSecretCapsuleSummary (capsuleId: Long) : RetrofitResult { + return apiHandler({ api.getSecretCapsuleSummaryApi(capsuleId) }) { response: ResponseBody -> response.result.toModel()} } override suspend fun modifySecretCapsule( - capsuleId: Int, + capsuleId: Long, request: SecretCapsuleModifyRequestDto ): RetrofitResult { return apiHandler({ api.modifySecretCapsuleApi(capsuleId , request) }) { response: ResponseBody -> response.result.toModel() } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleService.kt index de77a9958..c5d882fd7 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleService.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleService.kt @@ -18,12 +18,34 @@ interface CapsuleService { @Path("capsule_id") capsuleId : Long, ) : Response> - @GET("capsules/nearby") - suspend fun getNearbyCapsulesApi( + @GET("capsules/my/map/nearby") + suspend fun getNearbyMyCapsulesHomeApi( + @Query("latitude") latitude : Double, + @Query("longitude") longitude : Double, + @Query("distance") distance : Double, + @Query("capsule_type") capsuleType : String + ) : Response> + + @GET("capsules/friends/map/nearby") + suspend fun getNearbyFriendsCapsulesHomeApi( + @Query("latitude") latitude : Double, + @Query("longitude") longitude : Double, + @Query("distance") distance : Double, + ) : Response> + + @GET("capsules/my/ar/nearby") + suspend fun getNearbyMyCapsulesARApi( + @Query("latitude") latitude : Double, + @Query("longitude") longitude : Double, + @Query("distance") distance : Double, + @Query("capsule_type") capsuleType : String + ) : Response> + + @GET("capsules/friends/ar/nearby") + suspend fun getNearbyFriendsCapsulesARApi( @Query("latitude") latitude : Double, @Query("longitude") longitude : Double, @Query("distance") distance : Double, - @Query("capsule_type") capsule_type : String ) : Response> @GET("map/full-address") diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleSkinService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleSkinService.kt index be17ccd79..c6babe0c1 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleSkinService.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/CapsuleSkinService.kt @@ -24,7 +24,7 @@ interface CapsuleSkinService { @GET("capsule-skins") suspend fun getCapsuleSkinsPageApi( @Query("size") size : Int, - @Query("createdAt") createdAt: String + @Query("created_at") createdAt: String ) : Response> @POST("capsule-skins") diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/FriendService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/FriendService.kt new file mode 100644 index 000000000..dd35f98f9 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/FriendService.kt @@ -0,0 +1,67 @@ +package com.droidblossom.archive.data.source.remote.api + +import com.droidblossom.archive.data.dto.friend.request.FriendReqRequestDto +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.Response +import com.droidblossom.archive.data.dto.ResponseBody +import com.droidblossom.archive.data.dto.friend.request.FriendsReqRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchPhoneRequestDto +import com.droidblossom.archive.data.dto.friend.response.FriendReqStatusResponseDto +import com.droidblossom.archive.data.dto.friend.response.FriendsPageResponseDto +import com.droidblossom.archive.data.dto.friend.response.FriendsSearchPhoneResponseDto +import com.droidblossom.archive.data.dto.friend.response.FriendsSearchResponseDto +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface FriendService { + + @POST("friends/{friend_id}/request") + suspend fun postFriendsRequestApi( + @Path("friend_id") friendId : Long, + ) : Response> + + @POST("friends/{friend_id}/accept-request") + suspend fun postFriendsAcceptRequestApi( + @Path("friend_id") friendId : Long, + ) : Response> + + @GET("friends/search") + suspend fun postFriendsSearchApi( + @Query("friend_tag") friendTag : String + ) : Response> + + @POST("friends/search/phone") + suspend fun postFriendsSearchPhoneApi( + @Body request : FriendsSearchPhoneRequestDto + ) : Response> + + @GET("friends") + suspend fun getFriendsPageApi( + @Query("size") size : Int, + @Query("created_at") createdAt : String, + ) : Response> + + @GET("friends/requests") + suspend fun getFriendsRequestsPageApi( + @Query("size") size : Int, + @Query("created_at") createdAt : String, + ) : Response> + + @POST("friends/requests") + suspend fun postFriendListRequestsPageApi( + @Body request : FriendsReqRequestDto + ) : Response> + + @DELETE("friends/{friend_id}") + suspend fun deleteFriendApi( + @Path("friend_id") friendId : Long, + ) : Response> + + @DELETE("friends/{friend_id}/deny-request") + suspend fun deleteFriendDenyRequestApi( + @Path("friend_id") friendId : Long, + ) : Response> +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/MemberService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/MemberService.kt index f15ab10ac..b85c9e933 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/MemberService.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/MemberService.kt @@ -44,7 +44,7 @@ interface MemberService { @GET("me/notifications") suspend fun getNotifications( @Query("size") size : Int, - @Query("createdAt") createdAt : String + @Query("created_at") createdAt : String ): Response> diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/PublicService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/PublicService.kt new file mode 100644 index 000000000..9b08d0ac9 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/PublicService.kt @@ -0,0 +1,44 @@ +package com.droidblossom.archive.data.source.remote.api + +import com.droidblossom.archive.data.dto.ResponseBody +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.CapsuleDetailResponseDto +import com.droidblossom.archive.data.dto.common.CapsuleSummaryResponseDto +import com.droidblossom.archive.data.dto.open.response.MyPublicCapsulePageResponseDto +import com.droidblossom.archive.data.dto.open.response.PublicCapsuleSliceResponseDto +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface PublicService { + + @POST("public-capsules") + suspend fun postPublicCapsuleApi( + @Body request : CapsuleCreateRequestDto + ) : Response> + + @GET("public-capsules/{capsule_id}/summary") + suspend fun getPublicCapsuleSummaryApi( + @Path("capsule_id") capsuleId : Long, + ) : Response> + + @GET("public-capsules/{capsule_id}/detail") + suspend fun getPublicCapsuleDetailApi( + @Path("capsule_id") capsuleId : Long, + ) : Response> + + @GET("public-capsules") + suspend fun getPublicCapsulesPageApi( + @Query("size") size : Int, + @Query("created_at") createdAt: String + ) : Response> + + @GET("public-capsules/my") + suspend fun getMyPublicCapsulePageApi( + @Query("size") size : Int, + @Query("created_at") createdAt: String + ) : Response> +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/SecretService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/SecretService.kt index 155ffe15c..d1469b508 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/SecretService.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/data/source/remote/api/SecretService.kt @@ -1,14 +1,12 @@ package com.droidblossom.archive.data.source.remote.api import com.droidblossom.archive.data.dto.ResponseBody -import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleModifyRequestDto -import com.droidblossom.archive.data.dto.secret.request.SecretCapsulePageRequestDto -import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleCreateResponseDto -import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleDetailResponseDto +import com.droidblossom.archive.data.dto.common.CapsuleDetailResponseDto import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleModifyResponseDto import com.droidblossom.archive.data.dto.secret.response.SecretCapsulePageResponseDto -import com.droidblossom.archive.data.dto.secret.response.SecretCapsuleSummaryResponseDto +import com.droidblossom.archive.data.dto.common.CapsuleSummaryResponseDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET @@ -19,31 +17,31 @@ import retrofit2.http.Query interface SecretService { - @GET("secret/capsules") + @GET("secret-capsules") suspend fun getSecretCapsulePageApi( @Query("size") size : Int, - @Query("createdAt") createdAt: String + @Query("created_at") createdAt: String ) : Response> - @POST("secret/capsules") + @POST("secret-capsules") suspend fun postSecretCapsuleApi( - @Body request : SecretCapsuleCreateRequestDto + @Body request : CapsuleCreateRequestDto ) : Response> - @GET("secret/capsules/{capsule_id}/detail") + @GET("secret-capsules/{capsule_id}/detail") suspend fun getSecretCapsuleDetailApi( @Path("capsule_id") capsuleId : Long, - ) : Response> + ) : Response> - @GET("secret/capsules/{capsule_id}/summary") + @GET("secret-capsules/{capsule_id}/summary") suspend fun getSecretCapsuleSummaryApi( - @Path("capsule_id") capsuleId : Int, - ) : Response> + @Path("capsule_id") capsuleId : Long, + ) : Response> //미정 (1/27 기준) - @PATCH("secret/capsules/{capsule_id}") + @PATCH("secret-capsules/{capsule_id}") suspend fun modifySecretCapsuleApi( - @Path("capsule_id") capsuleId : Int, + @Path("capsule_id") capsuleId : Long, @Body request : SecretCapsuleModifyRequestDto ) : Response> diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RepositoryModule.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RepositoryModule.kt index efcae288e..f84ad067b 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RepositoryModule.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RepositoryModule.kt @@ -3,22 +3,28 @@ package com.droidblossom.archive.di import com.droidblossom.archive.data.repository.AuthRepositoryImpl import com.droidblossom.archive.data.repository.CapsuleRepositoryImpl import com.droidblossom.archive.data.repository.CapsuleSkinRepositoryImpl +import com.droidblossom.archive.data.repository.FriendRepositoryImpl import com.droidblossom.archive.data.repository.MemberRepositoryImpl import com.droidblossom.archive.data.repository.KakaoRepositoryImpl +import com.droidblossom.archive.data.repository.PublicRepositoryImpl import com.droidblossom.archive.data.repository.S3RepositoryImpl import com.droidblossom.archive.data.repository.SecretRepositoryImpl import com.droidblossom.archive.data.source.remote.api.AuthService import com.droidblossom.archive.data.source.remote.api.CapsuleService import com.droidblossom.archive.data.source.remote.api.CapsuleSkinService +import com.droidblossom.archive.data.source.remote.api.FriendService import com.droidblossom.archive.data.source.remote.api.MemberService import com.droidblossom.archive.data.source.remote.api.KakaoService +import com.droidblossom.archive.data.source.remote.api.PublicService import com.droidblossom.archive.data.source.remote.api.S3Service import com.droidblossom.archive.data.source.remote.api.SecretService import com.droidblossom.archive.domain.repository.AuthRepository import com.droidblossom.archive.domain.repository.CapsuleRepository import com.droidblossom.archive.domain.repository.CapsuleSkinRepository +import com.droidblossom.archive.domain.repository.FriendRepository import com.droidblossom.archive.domain.repository.MemberRepository import com.droidblossom.archive.domain.repository.KakaoRepository +import com.droidblossom.archive.domain.repository.PublicRepository import com.droidblossom.archive.domain.repository.S3Repository import com.droidblossom.archive.domain.repository.SecretRepository import dagger.Module @@ -64,4 +70,12 @@ object RepositoryModule { @ViewModelScoped fun providesCapsuleSkinRepository(api : CapsuleSkinService) : CapsuleSkinRepository = CapsuleSkinRepositoryImpl(api) + @Provides + @ViewModelScoped + fun providesFriendRepository(api : FriendService) : FriendRepository = FriendRepositoryImpl(api) + + @Provides + @ViewModelScoped + fun providesPublicRepository(api : PublicService) : PublicRepository = PublicRepositoryImpl(api) + } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RetrofitModule.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RetrofitModule.kt index f1a595c3d..1308a9931 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RetrofitModule.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/RetrofitModule.kt @@ -80,7 +80,7 @@ object RetrofitModule { gsonConverterFactory: GsonConverterFactory ): Retrofit { return Retrofit.Builder() - .baseUrl(BuildConfig.BASE_URL) + .baseUrl("${BuildConfig.BASE_URL}api/") .addConverterFactory(gsonConverterFactory) .client(client) .build() diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/ServiceModule.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/ServiceModule.kt index 6f4dc0f5f..7f0cbc53a 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/ServiceModule.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/di/ServiceModule.kt @@ -3,7 +3,9 @@ package com.droidblossom.archive.di import com.droidblossom.archive.data.source.remote.api.AuthService import com.droidblossom.archive.data.source.remote.api.CapsuleService import com.droidblossom.archive.data.source.remote.api.CapsuleSkinService +import com.droidblossom.archive.data.source.remote.api.FriendService import com.droidblossom.archive.data.source.remote.api.MemberService +import com.droidblossom.archive.data.source.remote.api.PublicService import com.droidblossom.archive.data.source.remote.api.S3Service import com.droidblossom.archive.data.source.remote.api.SecretService import dagger.Module @@ -41,4 +43,12 @@ object ServiceModule { @Singleton @Provides fun providesCapsuleSkinService(retrofit: Retrofit) : CapsuleSkinService = retrofit.create(CapsuleSkinService::class.java) + + @Singleton + @Provides + fun providesFriendService(retrofit : Retrofit) : FriendService = retrofit.create(FriendService::class.java) + + @Singleton + @Provides + fun providesPublicService(retrofit: Retrofit) : PublicService = retrofit.create(PublicService::class.java) } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleMarker.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleAnchor.kt similarity index 73% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleMarker.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleAnchor.kt index 088b5b7ac..89df1c99b 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleMarker.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleAnchor.kt @@ -1,8 +1,8 @@ -package com.droidblossom.archive.domain.model.common +package com.droidblossom.archive.domain.model.capsule import com.droidblossom.archive.presentation.ui.home.HomeFragment -data class CapsuleMarker( +data class CapsuleAnchor( val id : Long, val longitude : Double, val latitude : Double, diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleAnchors.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleAnchors.kt new file mode 100644 index 000000000..92c9081f2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleAnchors.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.domain.model.capsule + +data class CapsuleAnchors( + val capsuleAnchors : List +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleMarker.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleMarker.kt new file mode 100644 index 000000000..f1ef2f5f2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleMarker.kt @@ -0,0 +1,10 @@ +package com.droidblossom.archive.domain.model.capsule + +import com.droidblossom.archive.presentation.ui.home.HomeFragment + +data class CapsuleMarker ( + val id : Long, + val longitude : Double, + val latitude : Double, + val capsuleType : HomeFragment.CapsuleType, +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleMarkers.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleMarkers.kt new file mode 100644 index 000000000..f59a8eda4 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/CapsuleMarkers.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.domain.model.capsule + +data class CapsuleMarkers( + val capsuleMarkers : List +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/NearbyCapsule.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/NearbyCapsule.kt deleted file mode 100644 index 2b72dcbb1..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule/NearbyCapsule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.droidblossom.archive.domain.model.capsule - -import com.droidblossom.archive.data.dto.common.CapsuleSummaryDto -import com.droidblossom.archive.domain.model.common.CapsuleMarker - -data class NearbyCapsule( - val capsules : List -) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule_skin/CapsuleSkinsPageRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule_skin/CapsuleSkinsPageRequest.kt deleted file mode 100644 index b6d71c397..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule_skin/CapsuleSkinsPageRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.droidblossom.archive.domain.model.capsule_skin - -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto - -data class CapsuleSkinsPageRequest( - val size : Int, - val createdAt: String -){ - fun toDto()= CapsuleSkinsPageRequestDto( - size = this.size, - createdAt = this.createdAt - ) -} - diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule_skin/SkinMotion.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule_skin/SkinMotion.kt new file mode 100644 index 000000000..164476980 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/capsule_skin/SkinMotion.kt @@ -0,0 +1,12 @@ +package com.droidblossom.archive.domain.model.capsule_skin + +import com.droidblossom.archive.util.Motion +import com.droidblossom.archive.util.Retarget + +data class SkinMotion( + val id : Long, + val motionUrl: String, + val motionName: Motion, + val retarget: Retarget, + var isClicked :Boolean +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleCreateRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleCreateRequest.kt similarity index 66% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleCreateRequest.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleCreateRequest.kt index 3274632f4..81fa6f1e7 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleCreateRequest.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleCreateRequest.kt @@ -1,10 +1,8 @@ -package com.droidblossom.archive.domain.model.secret +package com.droidblossom.archive.domain.model.common -import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleCreateRequestDto -import com.droidblossom.archive.domain.model.common.AddressData -import com.droidblossom.archive.domain.model.common.FileName +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto -data class SecretCapsuleCreateRequest( +data class CapsuleCreateRequest( val capsuleSkinId: Long, val content: String, val directory: String, @@ -16,7 +14,7 @@ data class SecretCapsuleCreateRequest( val longitude: Double, val title: String ) { - fun toDto() = SecretCapsuleCreateRequestDto( + fun toDto() = CapsuleCreateRequestDto( capsuleSkinId = this.capsuleSkinId, content = this.content, directory = this.directory, diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleDetail.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleDetail.kt similarity index 67% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleDetail.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleDetail.kt index 79955443d..873893dad 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleDetail.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleDetail.kt @@ -1,8 +1,9 @@ -package com.droidblossom.archive.domain.model.secret +package com.droidblossom.archive.domain.model.common // 모든 캡슐 디테일로 통합될 예정 maybe -data class SecretCapsuleDetail( +data class CapsuleDetail( val address: String, + val roadName: String, val capsuleSkinUrl: String, val content: String, val createdDate: String, @@ -15,5 +16,5 @@ data class SecretCapsuleDetail( val title: String, val capsuleType: String ){ - constructor() : this("","","","",null,"",false, listOf(), listOf(),"","","") + constructor() : this("","", "","","",null,"",false, listOf(), listOf(),"","","") } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleSummary.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleSummaryResponse.kt similarity index 72% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleSummary.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleSummaryResponse.kt index a2183c694..eec832e69 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsuleSummary.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/CapsuleSummaryResponse.kt @@ -1,6 +1,6 @@ -package com.droidblossom.archive.domain.model.secret +package com.droidblossom.archive.domain.model.common -data class SecretCapsuleSummary( +data class CapsuleSummaryResponse( val nickname: String, val profileUrl: String, val skinUrl: String, diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/MyCapsule.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/MyCapsule.kt index b6332a421..1b537f91e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/MyCapsule.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/MyCapsule.kt @@ -1,11 +1,23 @@ package com.droidblossom.archive.domain.model.common +import com.droidblossom.archive.presentation.model.mypage.CapsuleData + data class MyCapsule ( val capsuleId: Long, val capsuleSkinUrl: String, val createdDate: String, val dueDate: String?, - val isOpened: Boolean, + var isOpened: Boolean, val title: String, val capsuleType: String -) \ No newline at end of file +){ + fun toUIModel() = CapsuleData( + capsuleId = this.capsuleId, + capsuleSkinUrl = this.capsuleSkinUrl, + createdDate = this.createdDate, + dueDate = this.dueDate, + isOpened = this.isOpened, + title = this.title, + capsuleType = this.capsuleType + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/SocialCapsules.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/SocialCapsules.kt new file mode 100644 index 000000000..71be14ef2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/common/SocialCapsules.kt @@ -0,0 +1,19 @@ +package com.droidblossom.archive.domain.model.common + +data class SocialCapsules ( + val capsuleId:Long, + val title:String, + val content:String, + val createdDate:String, + val dueDate:String?, + val profileUrl:String, + val capsuleSkinUrl: String, + val nickNameOrGroupName:String, + val thumbnailImage:String, + val roadName: String, + val address: String, + val hasThumbnail: Boolean, + val isOpened:Boolean, +) + +//val capsuleType: String, \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/Friend.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/Friend.kt new file mode 100644 index 000000000..2eb45efcd --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/Friend.kt @@ -0,0 +1,9 @@ +package com.droidblossom.archive.domain.model.friend + +data class Friend( + val createdAt: String, + val id: Long, + val nickname: String, + val profileUrl: String, + var isOpenDelete : Boolean, +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendAcceptRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendAcceptRequest.kt new file mode 100644 index 000000000..e04c42203 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendAcceptRequest.kt @@ -0,0 +1,12 @@ +package com.droidblossom.archive.domain.model.friend + +import com.droidblossom.archive.data.dto.friend.request.FriendAcceptRequestDto + + +data class FriendAcceptRequest ( + val friendId : Long +){ + fun toDto() = FriendAcceptRequestDto( + friendId = this.friendId, + ) +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendReqRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendReqRequest.kt new file mode 100644 index 000000000..f98361009 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendReqRequest.kt @@ -0,0 +1,11 @@ +package com.droidblossom.archive.domain.model.friend + +import com.droidblossom.archive.data.dto.friend.request.FriendReqRequestDto + +data class FriendReqRequest( + val friendId : Long +){ + fun toDto() = FriendReqRequestDto( + friendId = this.friendId, + ) +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendReqStatusResponse.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendReqStatusResponse.kt new file mode 100644 index 000000000..ead942596 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendReqStatusResponse.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.domain.model.friend + +data class FriendReqStatusResponse ( + val httpStatus : String, + val result : String +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsPage.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsPage.kt new file mode 100644 index 000000000..eb580c5d3 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsPage.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.domain.model.friend + +data class FriendsPage( + val friends: List, + val hasNext: Boolean +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchPhoneRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchPhoneRequest.kt new file mode 100644 index 000000000..b13222d9c --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchPhoneRequest.kt @@ -0,0 +1,12 @@ +package com.droidblossom.archive.domain.model.friend + +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchPhoneRequestDto +import com.droidblossom.archive.data.dto.friend.request.PhoneBooks + +data class FriendsSearchPhoneRequest ( + val phoneBooks : List +){ + fun toDto() = FriendsSearchPhoneRequestDto( + phoneBooks = this.phoneBooks, + ) +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchPhoneResponse.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchPhoneResponse.kt new file mode 100644 index 000000000..616f3eb3e --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchPhoneResponse.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.domain.model.friend + + +data class FriendsSearchPhoneResponse ( + val friends : List +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchRequest.kt new file mode 100644 index 000000000..b976d11bb --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchRequest.kt @@ -0,0 +1,11 @@ +package com.droidblossom.archive.domain.model.friend + +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchRequestDto + +data class FriendsSearchRequest ( + val friendTag : String +){ + fun toDto() = FriendsSearchRequestDto( + friendTag = this.friendTag, + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchResponse.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchResponse.kt new file mode 100644 index 000000000..c7e86f647 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/friend/FriendsSearchResponse.kt @@ -0,0 +1,12 @@ +package com.droidblossom.archive.domain.model.friend + +data class FriendsSearchResponse ( + val id : Long, + val profileUrl : String, + val nickname : String, + val isFriend : Boolean, + var isFriendInviteToFriend : Boolean, + var isFriendInviteToMe : Boolean, + var isChecked : Boolean, + val name : String +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/MemberDetail.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/MemberDetail.kt index 69d401588..b23ee375e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/MemberDetail.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/MemberDetail.kt @@ -1,7 +1,20 @@ package com.droidblossom.archive.domain.model.member +import com.droidblossom.archive.presentation.model.mypage.ProfileData + data class MemberDetail( val nickname : String, val profileUrl : String, - val phone : String -) \ No newline at end of file + val tag : String, + val friendCount: Int, + val groupCount: Int +){ + fun toUIModel() = ProfileData( + profileId = 0, + nickname = this.nickname, + profileUrl = this.profileUrl, + tag = this.tag, + friendCount = this.friendCount, + groupCount = this.groupCount + ) +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotiCategoryName.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotiCategoryName.kt new file mode 100644 index 000000000..09bbf43e6 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotiCategoryName.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.domain.model.member + +enum class NotiCategoryName { + CAPSULE_SKIN, FRIEND_REQUEST, FRIEND_ACCEPT, GROUP_REQUEST, GROUP_ACCEPT +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotificationModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotificationModel.kt index 2fb43d844..0e1d1d03c 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotificationModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/member/NotificationModel.kt @@ -4,5 +4,7 @@ data class NotificationModel( val createdAt: String, val imageUrl: String, val text: String, - val title: String + val title: String, + val categoryName : NotiCategoryName, + val status : String ) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/open/PublicCapsuleSliceResponse.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/open/PublicCapsuleSliceResponse.kt new file mode 100644 index 000000000..833cad2e2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/open/PublicCapsuleSliceResponse.kt @@ -0,0 +1,8 @@ +package com.droidblossom.archive.domain.model.open + +import com.droidblossom.archive.domain.model.common.SocialCapsules + +data class PublicCapsuleSliceResponse ( + val publicCapsules: List, + val hasNext: Boolean +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/CapsulePageList.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/CapsulePageList.kt new file mode 100644 index 000000000..5a8841f34 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/CapsulePageList.kt @@ -0,0 +1,9 @@ +package com.droidblossom.archive.domain.model.secret + +import com.droidblossom.archive.presentation.model.mypage.CapsuleData + +data class CapsulePageList( + val capsules: List, + val hasNext: Boolean, + val hasPrevious: Boolean +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsulePage.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsulePage.kt deleted file mode 100644 index 136a23561..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsulePage.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.droidblossom.archive.domain.model.secret - -import com.droidblossom.archive.domain.model.common.MyCapsule - -data class SecretCapsulePage( - val capsules: List, - val hasNext: Boolean, - val hasPrevious: Boolean -) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsulePageRequest.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsulePageRequest.kt deleted file mode 100644 index 13c50f515..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/secret/SecretCapsulePageRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.droidblossom.archive.domain.model.secret - -import com.droidblossom.archive.data.dto.secret.request.SecretCapsulePageRequestDto - -data class SecretCapsulePageRequest( - val size : Int, - val createdAt: String -){ - fun toDto()= SecretCapsulePageRequestDto( - size = this.size, - createdAt = this.createdAt - ) -} - diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/Agree.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/Agree.kt new file mode 100644 index 000000000..693c3f7e4 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/Agree.kt @@ -0,0 +1,7 @@ +package com.droidblossom.archive.domain.model.setting + +data class Agree( + val title : String, + val content : String, + var isOpen : Boolean = false, +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/Notice.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/Notice.kt new file mode 100644 index 000000000..e8366db59 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/Notice.kt @@ -0,0 +1,8 @@ +package com.droidblossom.archive.domain.model.setting + +data class Notice( + val title: String, + val date : String, + val contents : List, + var isOpen : Boolean = false +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/NoticeContent.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/NoticeContent.kt new file mode 100644 index 000000000..ba0c1d3dc --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/model/setting/NoticeContent.kt @@ -0,0 +1,5 @@ +package com.droidblossom.archive.domain.model.setting + +data class NoticeContent( + val content: String, +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleRepository.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleRepository.kt index dc6e944d4..48e97bc0d 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleRepository.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleRepository.kt @@ -2,7 +2,8 @@ package com.droidblossom.archive.domain.repository import com.droidblossom.archive.domain.model.capsule.CapsuleImages import com.droidblossom.archive.domain.model.capsule.CapsuleOpenedResponse -import com.droidblossom.archive.domain.model.capsule.NearbyCapsule +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchors +import com.droidblossom.archive.domain.model.capsule.CapsuleMarkers import com.droidblossom.archive.domain.model.common.AddressData import com.droidblossom.archive.util.RetrofitResult @@ -12,12 +13,31 @@ interface CapsuleRepository { capsuleId : Long ): RetrofitResult - suspend fun NearbyCapsules( + suspend fun nearbyMyCapsulesHome( latitude: Double, longitude: Double, distance: Double, capsuleType: String - ): RetrofitResult + ): RetrofitResult + + suspend fun nearbyFriendsCapsulesHome( + latitude: Double, + longitude: Double, + distance: Double, + ): RetrofitResult + + suspend fun nearbyMyCapsulesAR( + latitude: Double, + longitude: Double, + distance: Double, + capsuleType: String + ): RetrofitResult + + suspend fun nearbyFriendsCapsulesAR( + latitude: Double, + longitude: Double, + distance: Double, + ): RetrofitResult suspend fun getAddress( latitude: Double, diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleSkinRepository.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleSkinRepository.kt index ca5a9c47b..3b7f9976d 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleSkinRepository.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/CapsuleSkinRepository.kt @@ -1,17 +1,16 @@ package com.droidblossom.archive.domain.repository import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsMakeRequestDto -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsSearchPageRequestDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsMakeResponse import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsPageResponse import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsSearchPageResponse -import com.droidblossom.archive.domain.model.common.CapsuleSkinSummary import com.droidblossom.archive.util.RetrofitResult interface CapsuleSkinRepository { - suspend fun getCapsuleSkinPage(request: CapsuleSkinsPageRequestDto) : RetrofitResult + suspend fun getCapsuleSkinPage(request: PagingRequestDto) : RetrofitResult suspend fun postCapsuleSkinMake(request: CapsuleSkinsMakeRequestDto) : RetrofitResult diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/FriendRepository.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/FriendRepository.kt new file mode 100644 index 000000000..d79d2ced2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/FriendRepository.kt @@ -0,0 +1,26 @@ +package com.droidblossom.archive.domain.repository + +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendAcceptRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendReqRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsReqRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchPhoneRequestDto +import com.droidblossom.archive.data.dto.friend.request.FriendsSearchRequestDto +import com.droidblossom.archive.domain.model.friend.FriendReqStatusResponse +import com.droidblossom.archive.domain.model.friend.FriendsPage +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneResponse +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse +import com.droidblossom.archive.util.RetrofitResult + +interface FriendRepository { + + suspend fun postFriendsRequest(request: FriendReqRequestDto) : RetrofitResult + suspend fun postFriendsListRequest(request: FriendsReqRequestDto) : RetrofitResult + suspend fun postFriendsAcceptRequest(request: FriendAcceptRequestDto) : RetrofitResult + suspend fun postFriendsSearch(request: FriendsSearchRequestDto) : RetrofitResult + suspend fun postFriendsSearchPhone(request : FriendsSearchPhoneRequestDto) : RetrofitResult + suspend fun getFriendsPage(request: PagingRequestDto) : RetrofitResult + suspend fun getFriendsRequestsPage(request: PagingRequestDto) : RetrofitResult + suspend fun deleteFriend(friendId: Long) : RetrofitResult + suspend fun deleteFriendDeny(friendId: Long) : RetrofitResult +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/MemberRepository.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/MemberRepository.kt index 4ccd43725..74ff6f581 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/MemberRepository.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/MemberRepository.kt @@ -1,5 +1,6 @@ package com.droidblossom.archive.domain.repository +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.data.dto.member.request.FcmTokenRequsetDto import com.droidblossom.archive.data.dto.member.request.MemberStatusRequestDto import com.droidblossom.archive.data.dto.member.request.NotificationEnabledRequestDto @@ -20,7 +21,7 @@ interface MemberRepository { suspend fun patchFcmToken(request: FcmTokenRequsetDto): RetrofitResult - suspend fun getNotifications(size:Int, createdAt: String) : RetrofitResult + suspend fun getNotifications(request: PagingRequestDto) : RetrofitResult suspend fun getText() : RetrofitResult } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/PublicRepository.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/PublicRepository.kt new file mode 100644 index 000000000..7767336d9 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/PublicRepository.kt @@ -0,0 +1,22 @@ +package com.droidblossom.archive.domain.repository + +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.common.CapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse +import com.droidblossom.archive.domain.model.open.PublicCapsuleSliceResponse +import com.droidblossom.archive.domain.model.secret.CapsulePageList +import com.droidblossom.archive.util.RetrofitResult + +interface PublicRepository { + + suspend fun createPublicCapsule (request: CapsuleCreateRequestDto) : RetrofitResult + + suspend fun getPublicCapsuleSummary (capsuleId: Long) : RetrofitResult + + suspend fun getPublicCapsuleDetail (capsuleId: Long) : RetrofitResult + + suspend fun getPublicCapsulesPage(request: PagingRequestDto) : RetrofitResult + + suspend fun getMyPublicCapsulesPage(request: PagingRequestDto) : RetrofitResult +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/SecretRepository.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/SecretRepository.kt index 8fcb0c6d0..f1246ad52 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/SecretRepository.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/repository/SecretRepository.kt @@ -1,22 +1,22 @@ package com.droidblossom.archive.domain.repository -import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.CapsuleCreateRequestDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleModifyRequestDto -import com.droidblossom.archive.data.dto.secret.request.SecretCapsulePageRequestDto -import com.droidblossom.archive.domain.model.secret.SecretCapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleDetail import com.droidblossom.archive.domain.model.secret.SecretCapsuleModify -import com.droidblossom.archive.domain.model.secret.SecretCapsulePage -import com.droidblossom.archive.domain.model.secret.SecretCapsuleSummary +import com.droidblossom.archive.domain.model.secret.CapsulePageList +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse import com.droidblossom.archive.util.RetrofitResult interface SecretRepository { - suspend fun getSecretCapsulePage (request: SecretCapsulePageRequestDto) : RetrofitResult + suspend fun getSecretCapsulePage (request: PagingRequestDto) : RetrofitResult - suspend fun createSecretCapsule (request: SecretCapsuleCreateRequestDto) : RetrofitResult + suspend fun createSecretCapsule (request: CapsuleCreateRequestDto) : RetrofitResult - suspend fun getSecretCapsuleDetail (capsuleId: Long) : RetrofitResult + suspend fun getSecretCapsuleDetail (capsuleId: Long) : RetrofitResult - suspend fun getSecretCapsuleSummary (capsuleId: Int) : RetrofitResult - suspend fun modifySecretCapsule (capsuleId: Int, request: SecretCapsuleModifyRequestDto) : RetrofitResult + suspend fun getSecretCapsuleSummary (capsuleId: Long) : RetrofitResult + suspend fun modifySecretCapsule (capsuleId: Long, request: SecretCapsuleModifyRequestDto) : RetrofitResult } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/GetCapsuleImagesUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/GetCapsuleImagesUseCase.kt index 06458bf8c..55d040eeb 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/GetCapsuleImagesUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/GetCapsuleImagesUseCase.kt @@ -30,4 +30,5 @@ class GetCapsuleImagesUseCase @Inject constructor( } } -} \ No newline at end of file +} + diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyFriendsCapsulesARUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyFriendsCapsulesARUseCase.kt new file mode 100644 index 000000000..8cb995d12 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyFriendsCapsulesARUseCase.kt @@ -0,0 +1,32 @@ +package com.droidblossom.archive.domain.usecase.capsule + +import android.util.Log +import com.droidblossom.archive.domain.repository.CapsuleRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class NearbyFriendsCapsulesARUseCase @Inject constructor( + private val repository: CapsuleRepository +) { + + suspend operator fun invoke(latitude: Double, longitude: Double, distance: Double) = + flow { + try { + emit(repository.nearbyFriendsCapsulesAR(latitude, longitude, distance).onSuccess { + Log.d("성패", "$it") + }.onFail { + Log.d("실패", "$it") + }.onException { + Log.d("실패", "$it") + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyFriendsCapsulesHomeUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyFriendsCapsulesHomeUseCase.kt new file mode 100644 index 000000000..3b7870152 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyFriendsCapsulesHomeUseCase.kt @@ -0,0 +1,32 @@ +package com.droidblossom.archive.domain.usecase.capsule + +import android.util.Log +import com.droidblossom.archive.domain.repository.CapsuleRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class NearbyFriendsCapsulesHomeUseCase @Inject constructor( + private val repository: CapsuleRepository +) { + + suspend operator fun invoke(latitude: Double, longitude: Double, distance: Double) = + flow { + try { + emit(repository.nearbyFriendsCapsulesHome(latitude, longitude, distance).onSuccess { + Log.d("성패", "$it") + }.onFail { + Log.d("실패", "$it") + }.onException { + Log.d("실패", "$it") + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyCapsulesUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyMyCapsulesARUseCase.kt similarity index 85% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyCapsulesUseCase.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyMyCapsulesARUseCase.kt index 4d8ea98c3..770acac9b 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyCapsulesUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyMyCapsulesARUseCase.kt @@ -8,14 +8,14 @@ import com.droidblossom.archive.util.onSuccess import kotlinx.coroutines.flow.flow import javax.inject.Inject -class NearbyCapsulesUseCase @Inject constructor( +class NearbyMyCapsulesARUseCase @Inject constructor( private val repository: CapsuleRepository ) { suspend operator fun invoke(latitude: Double, longitude: Double, distance: Double, capsuleType: String) = flow { try { - emit(repository.NearbyCapsules(latitude, longitude, distance, capsuleType).onSuccess { + emit(repository.nearbyMyCapsulesAR(latitude, longitude, distance, capsuleType).onSuccess { Log.d("성패", "$it") }.onFail { Log.d("실패", "$it") diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyMyCapsulesHomeUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyMyCapsulesHomeUseCase.kt new file mode 100644 index 000000000..e54737672 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule/NearbyMyCapsulesHomeUseCase.kt @@ -0,0 +1,32 @@ +package com.droidblossom.archive.domain.usecase.capsule + +import android.util.Log +import com.droidblossom.archive.domain.repository.CapsuleRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class NearbyMyCapsulesHomeUseCase @Inject constructor( + private val repository: CapsuleRepository +) { + + suspend operator fun invoke(latitude: Double, longitude: Double, distance: Double, capsuleType: String) = + flow { + try { + emit(repository.nearbyMyCapsulesHome(latitude, longitude, distance, capsuleType).onSuccess { + Log.d("성패", "$it") + }.onFail { + Log.d("실패", "$it") + }.onException { + Log.d("실패", "$it") + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsMakeUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsMakeUseCase.kt index 9b969a097..a95ab7af3 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsMakeUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsMakeUseCase.kt @@ -2,7 +2,6 @@ package com.droidblossom.archive.domain.usecase.capsule_skin import android.util.Log import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsMakeRequestDto -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto import com.droidblossom.archive.domain.repository.CapsuleSkinRepository import com.droidblossom.archive.util.onException import com.droidblossom.archive.util.onFail diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsPageUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsPageUseCase.kt index 686260f9a..66035b59f 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsPageUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsPageUseCase.kt @@ -1,7 +1,7 @@ package com.droidblossom.archive.domain.usecase.capsule_skin import android.util.Log -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.domain.repository.CapsuleSkinRepository import com.droidblossom.archive.util.onException import com.droidblossom.archive.util.onFail @@ -13,7 +13,7 @@ class CapsuleSkinsPageUseCase @Inject constructor( private val repository: CapsuleSkinRepository ){ - suspend operator fun invoke(request: CapsuleSkinsPageRequestDto) = + suspend operator fun invoke(request: PagingRequestDto) = flow{ try { emit(repository.getCapsuleSkinPage(request).onSuccess { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsSearchUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsSearchUseCase.kt index 54bf855d7..cdb932876 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsSearchUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/capsule_skin/CapsuleSkinsSearchUseCase.kt @@ -1,7 +1,6 @@ package com.droidblossom.archive.domain.usecase.capsule_skin import android.util.Log -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsSearchPageRequestDto import com.droidblossom.archive.domain.repository.CapsuleSkinRepository import com.droidblossom.archive.util.onException diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendDeleteUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendDeleteUseCase.kt new file mode 100644 index 000000000..098abf9a9 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendDeleteUseCase.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.domain.model.friend.FriendAcceptRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendDeleteUseCase @Inject constructor( + private val repository: FriendRepository +) { + suspend operator fun invoke(friendId: Long) = + flow { + try { + emit(repository.deleteFriend(friendId).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendDenyRequestUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendDenyRequestUseCase.kt new file mode 100644 index 000000000..45dab6896 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendDenyRequestUseCase.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.domain.model.friend.FriendAcceptRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendDenyRequestUseCase @Inject constructor( + private val repository: FriendRepository +) { + suspend operator fun invoke(friendId: Long) = + flow { + try { + emit(repository.deleteFriendDeny(friendId).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendListRequestUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendListRequestUseCase.kt new file mode 100644 index 000000000..664fcf167 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendListRequestUseCase.kt @@ -0,0 +1,32 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.data.dto.friend.request.FriendsReqRequestDto +import com.droidblossom.archive.domain.model.friend.FriendReqRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendListRequestUseCase @Inject constructor( + private val repository: FriendRepository +) { + + suspend operator fun invoke(request : FriendsReqRequestDto) = + flow { + try { + emit(repository.postFriendsListRequest(request).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsAcceptRequestUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsAcceptRequestUseCase.kt new file mode 100644 index 000000000..38283d001 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsAcceptRequestUseCase.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.domain.model.friend.FriendAcceptRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendsAcceptRequestUseCase @Inject constructor( + private val repository: FriendRepository +) { + suspend operator fun invoke(request : FriendAcceptRequest) = + flow { + try { + emit(repository.postFriendsAcceptRequest(request.toDto()).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsPageUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsPageUseCase.kt new file mode 100644 index 000000000..70b3a1609 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsPageUseCase.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendsPageUseCase @Inject constructor( + private val repository: FriendRepository +) { + suspend operator fun invoke(request: PagingRequestDto) = + flow { + try { + emit(repository.getFriendsPage(request).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsRequestUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsRequestUseCase.kt new file mode 100644 index 000000000..734841e30 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsRequestUseCase.kt @@ -0,0 +1,31 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.domain.model.friend.FriendReqRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendsRequestUseCase @Inject constructor( + private val repository: FriendRepository +) { + + suspend operator fun invoke(request : FriendReqRequest) = + flow { + try { + emit(repository.postFriendsRequest(request.toDto()).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsRequestsPageUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsRequestsPageUseCase.kt new file mode 100644 index 000000000..f4ad6f920 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsRequestsPageUseCase.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendsRequestsPageUseCase @Inject constructor( + private val repository: FriendRepository +) { + suspend operator fun invoke(request: PagingRequestDto) = + flow { + try { + emit(repository.getFriendsRequestsPage(request).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsSearchPhoneUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsSearchPhoneUseCase.kt new file mode 100644 index 000000000..b71b3f2b4 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsSearchPhoneUseCase.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendsSearchPhoneUseCase @Inject constructor( + private val repository: FriendRepository +){ + suspend operator fun invoke(request : FriendsSearchPhoneRequest) = + flow { + try { + emit(repository.postFriendsSearchPhone(request.toDto()).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsSearchUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsSearchUseCase.kt new file mode 100644 index 000000000..cdd70c91e --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/friend/FriendsSearchUseCase.kt @@ -0,0 +1,31 @@ +package com.droidblossom.archive.domain.usecase.friend + +import android.util.Log +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneRequest +import com.droidblossom.archive.domain.model.friend.FriendsSearchRequest +import com.droidblossom.archive.domain.repository.FriendRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FriendsSearchUseCase @Inject constructor( + private val repository: FriendRepository +) { + suspend operator fun invoke(request : FriendsSearchRequest) = + flow { + try { + emit(repository.postFriendsSearch(request.toDto()).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + }catch (e : Exception){ + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/GetNotificationsUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/GetNotificationsUseCase.kt index 16cde24cc..c11658d60 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/GetNotificationsUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/GetNotificationsUseCase.kt @@ -1,6 +1,7 @@ package com.droidblossom.archive.domain.usecase.member import android.util.Log +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.data.dto.member.request.FcmTokenRequsetDto import com.droidblossom.archive.data.dto.member.request.NotificationEnabledRequestDto import com.droidblossom.archive.domain.model.member.NotificationPage @@ -15,10 +16,10 @@ import javax.inject.Inject class GetNotificationsUseCase @Inject constructor( private val repository: MemberRepository ) { - suspend operator fun invoke(size: Int, createdAt : String) = + suspend operator fun invoke(request: PagingRequestDto) = flow> { try { - emit(repository.getNotifications(size = size, createdAt = createdAt) + emit(repository.getNotifications(request) .onSuccess { }.onFail { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/NotificationEnabledUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/NotificationEnabledUseCase.kt index 528da08fc..109a91632 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/NotificationEnabledUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/member/NotificationEnabledUseCase.kt @@ -1,7 +1,6 @@ package com.droidblossom.archive.domain.usecase.member import android.util.Log -import com.droidblossom.archive.data.dto.member.request.FcmTokenRequsetDto import com.droidblossom.archive.data.dto.member.request.NotificationEnabledRequestDto import com.droidblossom.archive.domain.repository.MemberRepository import com.droidblossom.archive.util.RetrofitResult @@ -14,14 +13,14 @@ import javax.inject.Inject class NotificationEnabledUseCase @Inject constructor( private val repository: MemberRepository ) { - suspend operator fun invoke(request: NotificationEnabledRequestDto) = + suspend operator fun invoke(enabled: Boolean) = flow> { try { - emit(repository.patchNotificationEnabled(request) + emit(repository.patchNotificationEnabled(NotificationEnabledRequestDto(enabled)) .onSuccess { - + Log.d("알림", "성공") }.onFail { - + Log.d("알림", "실녀") }.onException { throw Exception(it) }) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/MyPublicCapsulePageUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/MyPublicCapsulePageUseCase.kt new file mode 100644 index 000000000..55bee8c79 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/MyPublicCapsulePageUseCase.kt @@ -0,0 +1,32 @@ +package com.droidblossom.archive.domain.usecase.open + +import android.util.Log +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.secret.CapsulePageList +import com.droidblossom.archive.domain.repository.PublicRepository +import com.droidblossom.archive.util.RetrofitResult +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class MyPublicCapsulePageUseCase @Inject constructor( + private val repository: PublicRepository +) { + suspend operator fun invoke(request: PagingRequestDto) = + flow> { + try { + emit(repository.getMyPublicCapsulesPage(request).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleCreateUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleCreateUseCase.kt new file mode 100644 index 000000000..02e2df640 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleCreateUseCase.kt @@ -0,0 +1,31 @@ +package com.droidblossom.archive.domain.usecase.open + +import android.util.Log +import com.droidblossom.archive.domain.model.common.CapsuleCreateRequest +import com.droidblossom.archive.domain.repository.PublicRepository +import com.droidblossom.archive.util.RetrofitResult +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class PublicCapsuleCreateUseCase @Inject constructor( + private val repository: PublicRepository +) { + suspend operator fun invoke(request: CapsuleCreateRequest) = + flow> { + try { + emit(repository.createPublicCapsule(request.toDto()).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleDetailUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleDetailUseCase.kt new file mode 100644 index 000000000..16a25f340 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleDetailUseCase.kt @@ -0,0 +1,29 @@ +package com.droidblossom.archive.domain.usecase.open + +import android.util.Log +import com.droidblossom.archive.domain.repository.PublicRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class PublicCapsuleDetailUseCase @Inject constructor( + private val repository: PublicRepository +) { + suspend operator fun invoke(capsuleId :Long) = + flow { + try { + emit(repository.getPublicCapsuleDetail(capsuleId).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsulePageUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsulePageUseCase.kt new file mode 100644 index 000000000..a66ef398e --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsulePageUseCase.kt @@ -0,0 +1,32 @@ +package com.droidblossom.archive.domain.usecase.open + +import android.util.Log +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.open.PublicCapsuleSliceResponse +import com.droidblossom.archive.domain.repository.PublicRepository +import com.droidblossom.archive.util.RetrofitResult +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class PublicCapsulePageUseCase @Inject constructor( + private val repository: PublicRepository +) { + suspend operator fun invoke(request: PagingRequestDto) = + flow> { + try { + emit(repository.getPublicCapsulesPage(request).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleSummaryUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleSummaryUseCase.kt new file mode 100644 index 000000000..8663992ab --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/open/PublicCapsuleSummaryUseCase.kt @@ -0,0 +1,29 @@ +package com.droidblossom.archive.domain.usecase.open + +import android.util.Log +import com.droidblossom.archive.domain.repository.PublicRepository +import com.droidblossom.archive.util.onException +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class PublicCapsuleSummaryUseCase @Inject constructor( + private val repository: PublicRepository +) { + suspend operator fun invoke(capsuleId :Long) = + flow { + try { + emit(repository.getPublicCapsuleSummary(capsuleId).onSuccess { + + }.onFail { + + }.onException { + throw Exception(it) + }) + } catch (e: Exception) { + Log.d("예외확인", "$e") + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleCreateUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleCreateUseCase.kt index 0e9ff9f6e..a60c6ff35 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleCreateUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleCreateUseCase.kt @@ -1,9 +1,7 @@ package com.droidblossom.archive.domain.usecase.secret import android.util.Log -import com.droidblossom.archive.data.dto.secret.request.SecretCapsuleCreateRequestDto -import com.droidblossom.archive.domain.model.secret.SecretCapsuleCreateRequest -import com.droidblossom.archive.domain.model.secret.SecretCapsulePage +import com.droidblossom.archive.domain.model.common.CapsuleCreateRequest import com.droidblossom.archive.domain.repository.SecretRepository import com.droidblossom.archive.util.RetrofitResult import com.droidblossom.archive.util.onException @@ -15,7 +13,7 @@ import javax.inject.Inject class SecretCapsuleCreateUseCase @Inject constructor( private val repository: SecretRepository ) { - suspend operator fun invoke(request: SecretCapsuleCreateRequest) = + suspend operator fun invoke(request: CapsuleCreateRequest) = flow> { try { emit(repository.createSecretCapsule(request.toDto()).onSuccess { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleDetailUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleDetailUseCase.kt index 4f08516aa..aaf6c9f38 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleDetailUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleDetailUseCase.kt @@ -1,7 +1,7 @@ package com.droidblossom.archive.domain.usecase.secret import android.util.Log -import com.droidblossom.archive.domain.model.secret.SecretCapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleDetail import com.droidblossom.archive.domain.repository.SecretRepository import com.droidblossom.archive.util.RetrofitResult import com.droidblossom.archive.util.onException @@ -14,7 +14,7 @@ class SecretCapsuleDetailUseCase @Inject constructor( private val repository: SecretRepository ) { suspend operator fun invoke(capsuleId: Long) = - flow> { + flow> { try { emit(repository.getSecretCapsuleDetail(capsuleId).onSuccess { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleModifyUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleModifyUseCase.kt index 05c4ca50d..e90ee3e25 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleModifyUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleModifyUseCase.kt @@ -14,7 +14,7 @@ import javax.inject.Inject data class SecretCapsuleModifyUseCase @Inject constructor( private val repository: SecretRepository ) { - suspend operator fun invoke(capsuleId :Int,request: SecretCapsuleModifyRequestDto) = + suspend operator fun invoke(capsuleId :Long,request: SecretCapsuleModifyRequestDto) = flow> { try { emit(repository.modifySecretCapsule(capsuleId, request).onSuccess { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsulePageUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsulePageUseCase.kt index 7b2140feb..2a2fc2417 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsulePageUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsulePageUseCase.kt @@ -1,8 +1,8 @@ package com.droidblossom.archive.domain.usecase.secret import android.util.Log -import com.droidblossom.archive.data.dto.secret.request.SecretCapsulePageRequestDto -import com.droidblossom.archive.domain.model.secret.SecretCapsulePage +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.secret.CapsulePageList import com.droidblossom.archive.domain.repository.SecretRepository import com.droidblossom.archive.util.RetrofitResult import com.droidblossom.archive.util.onException @@ -14,8 +14,8 @@ import javax.inject.Inject class SecretCapsulePageUseCase @Inject constructor( private val repository: SecretRepository ) { - suspend operator fun invoke(request: SecretCapsulePageRequestDto) = - flow> { + suspend operator fun invoke(request: PagingRequestDto) = + flow> { try { emit(repository.getSecretCapsulePage(request).onSuccess { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleSummaryUseCase.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleSummaryUseCase.kt index 010291618..df094ed36 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleSummaryUseCase.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/domain/usecase/secret/SecretCapsuleSummaryUseCase.kt @@ -12,7 +12,7 @@ class SecretCapsuleSummaryUseCase @Inject constructor( private val repository : SecretRepository ) { - suspend operator fun invoke(capsuleId :Int) = + suspend operator fun invoke(capsuleId :Long) = flow { try { emit(repository.getSecretCapsuleSummary(capsuleId).onSuccess { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseActivity.kt index 48c9b33ba..61f4ece25 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseActivity.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseActivity.kt @@ -1,18 +1,33 @@ package com.droidblossom.archive.presentation.base +import android.Manifest +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.content.Intent import android.graphics.Color +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings import android.util.Log import android.view.View import android.view.WindowManager import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding -import com.droidblossom.archive.presentation.snack.CallSnackBar -import com.droidblossom.archive.presentation.snack.HomeSnackBarSmall +import androidx.fragment.app.DialogFragment +import com.droidblossom.archive.presentation.customview.HomeSnackBarSmall +import com.droidblossom.archive.presentation.customview.LoadingDialog +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment +import com.droidblossom.archive.presentation.model.AppEvent +import com.droidblossom.archive.util.ClipboardUtil import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -26,6 +41,17 @@ abstract class BaseActivity(@LayoutRes v private var _binding: V? = null protected val binding: V get() = _binding!! + private lateinit var loadingDialog: LoadingDialog + private var loadingState = false + + private lateinit var resultLauncher: ActivityResultLauncher + private lateinit var onSettingsResult: () -> Unit + + val essentialPermissionList = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) abstract fun observeData() protected fun getStatusBarHeight(): Int { @@ -42,15 +68,17 @@ abstract class BaseActivity(@LayoutRes v EventBus.getDefault().unregister(this); } - @Subscribe(threadMode = ThreadMode.MAIN) - fun printId(event: Map) { - // 스낵바 호출로 바꾸면 될듯? - Log.d("이베", "$event") - binding.root.let { rootView -> - HomeSnackBarSmall(rootView).show() + @Subscribe + fun onEvent(event: AppEvent) { + when (event) { + is AppEvent.NetworkDisconnectedEvent -> { + showToastMessage("네트워크") + } + is AppEvent.NotificationReceivedEvent -> { + showToastMessage("알림") + } } } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DataBindingUtil.setContentView(this, layoutResource) @@ -58,6 +86,11 @@ abstract class BaseActivity(@LayoutRes v setContentView(binding.root) fetchJob = viewModel?.fetchData() observeData() + + resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + onSettingsResult() + } + window.apply { decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR @@ -66,17 +99,58 @@ abstract class BaseActivity(@LayoutRes v } } + + + fun showLoading(context: Context) { + if (!loadingState) { + loadingDialog = LoadingDialog(context) + loadingDialog.show() + loadingState = true + } + } + + + fun dismissLoading() { + if (loadingState) { + loadingDialog.dismiss() + loadingState = false + } + } + override fun onDestroy() { fetchJob?.let { if (it.isActive) { it.cancel() } + dismissLoading() } super.onDestroy() } + fun copyText(label:String, text: String) { + ClipboardUtil.copyTextToClipboard(this, label, text) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2){ + showToastMessage("클립보드에 복사되었어요.") + } + } fun showToastMessage(message: String) { val toast = Toast.makeText(this, message, Toast.LENGTH_SHORT) toast.show() } + + fun showSettingsDialog(permissionType: PermissionDialogFragment.PermissionType, listener: PermissionDialogButtonClickListener) { + val existingDialog = supportFragmentManager.findFragmentByTag(PermissionDialogFragment.TAG) as DialogFragment? + if (existingDialog == null) { + val dialog = PermissionDialogFragment.newInstance(permissionType.name, listener) + dialog.show(supportFragmentManager, PermissionDialogFragment.TAG) + } + } + fun navigateToAppSettings(onComplete: () -> Unit) { + onSettingsResult = onComplete // 저장된 콜백 설정 + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + val uri = Uri.fromParts("package", packageName, null) + data = uri + } + resultLauncher.launch(intent) // 설정 화면으로 이동 + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseFragment.kt index 04aaa7a91..77615399e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseFragment.kt @@ -1,14 +1,28 @@ package com.droidblossom.archive.presentation.base +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings 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.contract.ActivityResultContracts import androidx.annotation.LayoutRes import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import com.droidblossom.archive.presentation.customview.LoadingDialog +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment +import com.droidblossom.archive.util.ClipboardUtil import kotlinx.coroutines.Job abstract class BaseFragment(@LayoutRes val layoutResource :Int): Fragment() { @@ -19,11 +33,22 @@ abstract class BaseFragment(@LayoutRes va private var _binding: V? = null protected val binding : V get() = _binding!! + private lateinit var loadingDialog: LoadingDialog + private var loadingState = false + + private lateinit var resultLauncher: ActivityResultLauncher + + val essentialPermissionList = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) protected fun getStatusBarHeight(): Int { val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 } + private lateinit var onSettingsResult: () -> Unit abstract fun observeData() override fun onCreateView( @@ -45,19 +70,65 @@ abstract class BaseFragment(@LayoutRes va override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fetchJob = viewModel.fetchData() + resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + onSettingsResult() + } observeData() } + fun copyText(label:String, text: String) { + ClipboardUtil.copyTextToClipboard(requireContext(), label, text) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2){ + showToastMessage("클립보드에 복사되었어요.") + } + } + fun showToastMessage(message: String) { val toast = Toast.makeText(activity, message, Toast.LENGTH_SHORT) toast.show() } + fun showSettingsDialog(permissionType: PermissionDialogFragment.PermissionType, listener: PermissionDialogButtonClickListener) { + + val existingDialog = parentFragmentManager.findFragmentByTag(PermissionDialogFragment.TAG) as DialogFragment? + + if (existingDialog == null) { + val dialog = PermissionDialogFragment.newInstance(permissionType.name, listener) + dialog.show(parentFragmentManager, PermissionDialogFragment.TAG) + } + + } + + fun navigateToAppSettings(onComplete: () -> Unit) { + onSettingsResult = onComplete // 저장된 콜백 설정 + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + val uri = Uri.fromParts("package", requireContext().packageName, null) + data = uri + } + resultLauncher.launch(intent) // 설정 화면으로 이동 + } + + fun showLoading(context: Context) { + if (!loadingState) { + loadingDialog = LoadingDialog(context) + loadingDialog.show() + loadingState = true + } + } + + fun dismissLoading() { + if (loadingState) { + loadingDialog.dismiss() + loadingState = false + } + } + override fun onDestroyView() { super.onDestroyView() if (fetchJob.isActive) { fetchJob.cancel() } + dismissLoading() _binding = null } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseViewModel.kt index 0af502b11..c5f156cdc 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/base/BaseViewModel.kt @@ -3,9 +3,26 @@ package com.droidblossom.archive.presentation.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit abstract class BaseViewModel:ViewModel() { open fun fetchData(): Job = viewModelScope.launch { } + + companion object { + fun Flow.throttleFirst(windowDuration: Long, timeUnit: TimeUnit): Flow = channelFlow { + val windowMillis = timeUnit.toMillis(windowDuration) + var lastTime = 0L + collect { value -> + val currentTime = System.currentTimeMillis() + if (currentTime - lastTime >= windowMillis) { + lastTime = currentTime + send(value) + } + } + } + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/CallSnackBar.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/CallSnackBar.kt similarity index 60% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/CallSnackBar.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/CallSnackBar.kt index bc2ce7aed..90cc3a8d3 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/CallSnackBar.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/CallSnackBar.kt @@ -1,4 +1,4 @@ -package com.droidblossom.archive.presentation.snack +package com.droidblossom.archive.presentation.customview data class CallSnackBar( // 타입, 아이디 등등 추가 diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/CommonDialogFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/CommonDialogFragment.kt new file mode 100644 index 000000000..7a56f9669 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/CommonDialogFragment.kt @@ -0,0 +1,56 @@ +package com.droidblossom.archive.presentation.customview + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentCommonDialogBinding +import com.droidblossom.archive.presentation.base.BaseDialogFragment + +class CommonDialogFragment( + private val onClick: () -> Unit +) : BaseDialogFragment(R.layout.fragment_common_dialog) { + + override fun onStart() { + super.onStart() + val dialog = dialog + if (dialog != null) { + val width = ViewGroup.LayoutParams.MATCH_PARENT + val height = ViewGroup.LayoutParams.WRAP_CONTENT + dialog.window?.setLayout(width, height) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.messageT.text = arguments?.getString("title") ?: "" + binding.rightBtn.text = arguments?.getString("rightBtnT") ?: "확인" + + binding.leftBtn.setOnClickListener { + this.dismiss() + } + + binding.rightBtn.setOnClickListener { + onClick() + this.dismiss() + } + } + + companion object { + + fun newIntent( + title: String, + rightBtnT: String, + onRightClick: () -> Unit + ): CommonDialogFragment { + val args = Bundle().apply { + putString("title", title) + putString("rightBtnT", rightBtnT) + } + return CommonDialogFragment(onRightClick).apply { + arguments = args + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/HomeSnackBarBig.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/HomeSnackBarBig.kt similarity index 96% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/HomeSnackBarBig.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/HomeSnackBarBig.kt index 8160f3c98..b756f77a5 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/HomeSnackBarBig.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/HomeSnackBarBig.kt @@ -1,4 +1,4 @@ -package com.droidblossom.archive.presentation.snack +package com.droidblossom.archive.presentation.customview import android.view.LayoutInflater import android.view.View diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/HomeSnackBarSmall.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/HomeSnackBarSmall.kt similarity index 96% rename from frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/HomeSnackBarSmall.kt rename to frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/HomeSnackBarSmall.kt index e3d76c0f9..01033771c 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/snack/HomeSnackBarSmall.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/HomeSnackBarSmall.kt @@ -1,4 +1,4 @@ -package com.droidblossom.archive.presentation.snack +package com.droidblossom.archive.presentation.customview import android.view.LayoutInflater diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/LoadingDialog.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/LoadingDialog.kt new file mode 100644 index 000000000..621c8297f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/LoadingDialog.kt @@ -0,0 +1,29 @@ +package com.droidblossom.archive.presentation.customview + +import android.app.Dialog +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.Window +import com.droidblossom.archive.databinding.DialogLoadingBinding + +class LoadingDialog(context : Context) : Dialog(context) { + + private lateinit var binding : DialogLoadingBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + binding = DialogLoadingBinding.inflate(layoutInflater) + setContentView(binding.root) + setCanceledOnTouchOutside(false) + setCancelable(false) + window!!.setBackgroundDrawable(ColorDrawable()) + window!!.setDimAmount(0.5f) + + } + + override fun show(){ + if(!this.isShowing) super.show() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/PermissionDialogFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/PermissionDialogFragment.kt new file mode 100644 index 000000000..2a4832589 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/PermissionDialogFragment.kt @@ -0,0 +1,118 @@ +package com.droidblossom.archive.presentation.customview + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.DialogPermissionBinding +import com.droidblossom.archive.presentation.base.BaseDialogFragment + +interface PermissionDialogButtonClickListener { + fun onLeftButtonClicked() + fun onRightButtonClicked() +} + +class PermissionDialogFragment( + private val listener: PermissionDialogButtonClickListener, +) : BaseDialogFragment(R.layout.dialog_permission) { + + override fun onStart() { + super.onStart() + val dialog = dialog + if (dialog != null) { + val width = ViewGroup.LayoutParams.MATCH_PARENT + val height = ViewGroup.LayoutParams.WRAP_CONTENT + dialog.window?.setLayout(width, height) + dialog.setCancelable(false) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val permissionType = arguments?.getString("permission") ?: "" + setupPermissionInfo(permissionType) + + binding.leftBtn.text = if (permissionType == PermissionType.ESSENTIAL.name) "앱 종료" else "취소" + + binding.leftBtn.setOnClickListener { + listener.onLeftButtonClicked() + this.dismiss() + } + + binding.rightBtn.setOnClickListener { + listener.onRightButtonClicked() + this.dismiss() + } + } + + private fun setupPermissionInfo(permissionType: String) { + val info = when (permissionType) { + PermissionType.LOCATION.name -> Pair( + "ARchive 은 위치 권한이 필수입니다. 앱을 사용하려면 위치 권한을 허용해 주세요.", + R.drawable.ic_location_outline + ) + PermissionType.CAMERA.name -> Pair( + "AR 기능을 사용하려면 카메라 권한이 필요합니다. '권한'에서 카메라 권한을 허용해 주세요.", + R.drawable.ic_camera_outline + ) + PermissionType.CONTACTS.name -> Pair( + "연락처를 통해 앱 내에서 친구를 찾아보세요. 연락처와 전화 권한을 허용하면 친구 찾기가 가능합니다.", + R.drawable.ic_contacts_outline + ) + PermissionType.NOTIFICATIONS.name -> Pair( + "ARchive 앱의 알림을 받기 위해서는 알림 권한이 필요합니다. 알림을 통해 중요한 정보와 업데이트를 놓치지 마세요.", + R.drawable.ic_alarm_24 + ) + PermissionType.CALL.name -> Pair( + "연락처를 통해 앱 내에서 친구를 찾아보세요. 전화 권한을 허용하면 친구 찾기가 가능합니다.", + R.drawable.ic_call_24 + ) + PermissionType.CONTACTS_AND_CALL.name -> Pair( + "연락처를 통해 앱 내에서 친구를 찾아보세요. 전화, 연락처 권한을 허용하면 친구 찾기가 가능합니다.", + R.drawable.ic_contact_phone_outline + ) + PermissionType.AR.name -> Pair( + "AR 기능을 사용하려면 카메라, 위치 권한이 필요합니다. '권한'에서 카메라, 위치 권한을 허용해 주세요.", + R.drawable.ic_ar_outline + ) + PermissionType.ESSENTIAL.name -> Pair( + "ARchive 앱을 사용하려면 카메라, 위치 권한이 필수입니다. '권한'에서 카메라, 위치 권한을 허용해 주세요.", + R.drawable.ic_essential_outline + ) + else -> Pair("", R.drawable.ic_default_outline) + } + + binding.messageT.text = info.first + if (info.second != 0) { + binding.permissionImg.setImageResource(info.second) + } + } + + companion object { + + const val TAG = "PERMISSION_DIALOG" + fun newInstance( + permission: String, + listener: PermissionDialogButtonClickListener + ): PermissionDialogFragment { + val args = Bundle().apply { + putString("permission", permission) + } + return PermissionDialogFragment(listener).apply { + arguments = args + } + } + } + + enum class PermissionType(val description: String) { + LOCATION("위치"), + CAMERA("카메라"), + CONTACTS("연락처"), + NOTIFICATIONS("알람"), + CALL("전화"), + CONTACTS_AND_CALL("연락처, 전화"), + AR("카메라, 위치"), + ESSENTIAL("필수") + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/SkinDialogFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/SkinDialogFragment.kt new file mode 100644 index 000000000..a9afa2cf4 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/customview/SkinDialogFragment.kt @@ -0,0 +1,4 @@ +package com.droidblossom.archive.presentation.customview + +class SkinDialogFragment { +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/AppEvent.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/AppEvent.kt new file mode 100644 index 000000000..77c34d07f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/AppEvent.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.presentation.model + +sealed class AppEvent { + object NetworkDisconnectedEvent : AppEvent() + data class NotificationReceivedEvent(val message: String) : AppEvent() +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/CapsuleData.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/CapsuleData.kt new file mode 100644 index 000000000..3bc471bf5 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/CapsuleData.kt @@ -0,0 +1,11 @@ +package com.droidblossom.archive.presentation.model.mypage + +data class CapsuleData ( + val capsuleId: Long, + val capsuleSkinUrl: String, + val createdDate: String, + val dueDate: String?, + var isOpened: Boolean, + val title: String, + val capsuleType: String +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/ProfileData.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/ProfileData.kt new file mode 100644 index 000000000..90e148544 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/ProfileData.kt @@ -0,0 +1,10 @@ +package com.droidblossom.archive.presentation.model.mypage + +data class ProfileData ( + val profileId:Long, + val nickname : String, + val profileUrl : String, + val tag : String, + val friendCount: Int, + val groupCount: Int +) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/SpinnerData.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/SpinnerData.kt new file mode 100644 index 000000000..3745cc72a --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/SpinnerData.kt @@ -0,0 +1,7 @@ +package com.droidblossom.archive.presentation.model.mypage + +import com.droidblossom.archive.presentation.ui.mypage.adapter.CapsuleTypeSpinner + +data class SpinnerData( + val capsuleType:CapsuleTypeSpinner +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/StoryData.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/StoryData.kt new file mode 100644 index 000000000..d4dcc86a1 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/model/mypage/StoryData.kt @@ -0,0 +1,6 @@ +package com.droidblossom.archive.presentation.model.mypage + +data class StoryData( + val id:Long, + val imageUrl:String +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainActivity.kt index 748e959ce..58cf5b3a4 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainActivity.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainActivity.kt @@ -1,18 +1,30 @@ package com.droidblossom.archive.presentation.ui +import android.Manifest import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.droidblossom.archive.R import com.droidblossom.archive.data.dto.member.request.FcmTokenRequsetDto import com.droidblossom.archive.databinding.ActivityMainBinding import com.droidblossom.archive.domain.usecase.member.FcmTokenUseCase import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment import com.droidblossom.archive.presentation.ui.camera.CameraFragment import com.droidblossom.archive.presentation.ui.home.HomeFragment import com.droidblossom.archive.presentation.ui.mypage.MyPageFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendActivity +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.FriendAcceptActivity import com.droidblossom.archive.presentation.ui.skin.SkinFragment import com.droidblossom.archive.presentation.ui.social.SocialFragment import com.droidblossom.archive.util.DataStoreUtils @@ -22,12 +34,12 @@ import com.droidblossom.archive.util.onSuccess import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint -class MainActivity : BaseActivity(R.layout.activity_main) { +class MainActivity : BaseActivity(R.layout.activity_main) { @Inject lateinit var fcmTokenUseCase: FcmTokenUseCase @@ -35,16 +47,224 @@ class MainActivity : BaseActivity(R.layout.activi @Inject lateinit var dataStoreUtils: DataStoreUtils - override val viewModel: Nothing? = null - lateinit var viewBinding: ActivityMainBinding + override val viewModel: MainViewModelImpl by viewModels() - override fun observeData() {} + private var isPermissionRequested = false + + + private val arPermissionList = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + + private val arPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + viewModel.setMainTab(MainPage.AR) + //showFragment(CameraFragment.newIntent(), CameraFragment.TAG) + //binding.bottomNavigation.selectedItemId = R.id.menuCamera + } + + permissions.none { it.value } -> { + handleAllPermissionsDenied() + } + + else -> { + handlePartialPermissionsDenied(permissions) + } + } + } + + private val essentialPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + + } + + else -> { + handleEssentialPermissionsDenied() + } + } + + } + + + private fun handleAllPermissionsDenied() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + ) { + showToastMessage("AR 기능을 사용하려면 카메라, 위치 권한이 필요합니다.") + } else { + showSettingsDialog( + PermissionDialogFragment.PermissionType.AR, + object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("AR 기능을 사용하려면 카메라, 위치 권한이 필요합니다.") + } + + override fun onRightButtonClicked() { + navigateToAppSettings { arPermissionLauncher.launch(arPermissionList) } + } + + }) + } + } + + private fun handlePartialPermissionsDenied(permissions: Map) { + permissions.forEach { (permission, granted) -> + if (!granted) { + when (permission) { + Manifest.permission.CAMERA -> showPermissionDialog(PermissionDialogFragment.PermissionType.CAMERA) + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION -> { + if (!permissions.getValue(Manifest.permission.ACCESS_FINE_LOCATION) || + !permissions.getValue(Manifest.permission.ACCESS_COARSE_LOCATION) + ) { + showPermissionDialog(PermissionDialogFragment.PermissionType.LOCATION) + } + } + } + } + } + } + + private fun handleEssentialPermissionsDenied() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + ) { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + } else { + showSettingsDialog(PermissionDialogFragment.PermissionType.ESSENTIAL, + object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + finish() + } + + override fun onRightButtonClicked() { + isPermissionRequested = false + navigateToAppSettings { + if (essentialPermissionList.any { + ActivityCompat.checkSelfPermission( + this@MainActivity, + it + ) != PackageManager.PERMISSION_GRANTED + }) { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + goMain(this@MainActivity) + } + } + } + + }) + } + } + + private fun showPermissionDialog(permissionType: PermissionDialogFragment.PermissionType) { + + if (shouldShowRequestPermissionRationale(permissionType.toString())) { + showToastMessage("AR 기능을 사용하려면 ${permissionType.description} 권한이 필요합니다.") + } else { + showSettingsDialog(permissionType, object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("AR 기능을 사용하려면 ${permissionType.description} 권한이 필요합니다.") + } + + override fun onRightButtonClicked() { + navigateToAppSettings { arPermissionLauncher.launch(arPermissionList) } + } + + }) + } + + } + + + override fun observeData() { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.selectedMainTab + .filterNotNull() + .collect { tab -> + dataStoreUtils.saveSelectedTab(tab.name) + Log.d("알림", tab.name) + when (tab) { + MainPage.HOME -> { + showFragment(HomeFragment.newIntent(), HomeFragment.TAG) + binding.bottomNavigation.selectedItemId = R.id.menuHome + } + + MainPage.SKIN -> { + showFragment(SkinFragment.newIntent(), SkinFragment.TAG) + binding.bottomNavigation.selectedItemId = R.id.menuSkin + } + + MainPage.AR -> { + showFragment(CameraFragment.newIntent(), CameraFragment.TAG) + binding.bottomNavigation.selectedItemId = R.id.menuCamera + } + + MainPage.SOCIAL -> { + showFragment(SocialFragment.newIntent(), SocialFragment.TAG) + binding.bottomNavigation.selectedItemId = R.id.menuSocial + } + + MainPage.MY_PAGE -> { + showFragment(MyPageFragment.newIntent(), MyPageFragment.TAG) + binding.bottomNavigation.selectedItemId = R.id.menuMyPage + } + + MainPage.NULL -> { + + } + } + } + } + } + + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.mainEvents.collect { event -> + when (event) { + MainViewModel.MainEvent.NavigateToHome -> { + viewModel.setMainTab(MainPage.HOME) + } + + MainViewModel.MainEvent.NavigateToSkin -> { + viewModel.setMainTab(MainPage.SKIN) + } + + MainViewModel.MainEvent.NavigateToCamera -> { + arPermissionLauncher.launch(arPermissionList) + } + + MainViewModel.MainEvent.NavigateToSocial -> { + viewModel.setMainTab(MainPage.SOCIAL) + } + + MainViewModel.MainEvent.NavigateToMyPage -> { + viewModel.setMainTab(MainPage.MY_PAGE) + } + + is MainViewModel.MainEvent.ShowToastMessage -> { + + } + } + } + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewBinding = binding - showFragment(HomeFragment.newIntent(), HomeFragment.TAG) + //showFragment(HomeFragment.newIntent(), HomeFragment.TAG) + Log.d("흠", "온 크리에이트 ${viewModel.selectedMainTab.value}") initBottomNav() initFCM() @@ -59,7 +279,7 @@ class MainActivity : BaseActivity(R.layout.activi } } - private fun initFCM(){ + private fun initFCM() { CoroutineScope(Dispatchers.IO).launch { fcmTokenUseCase( FcmTokenRequsetDto(MyFirebaseMessagingService().getFirebaseToken()) @@ -73,31 +293,32 @@ class MainActivity : BaseActivity(R.layout.activi } } - private fun initBottomNav(){ + private fun initBottomNav() { binding.fab.setOnClickListener { - showFragment(CameraFragment.newIntent(), CameraFragment.TAG) - binding.bottomNavigation.selectedItemId = R.id.menuCamera + viewModel.mainEvent(MainViewModel.MainEvent.NavigateToCamera) } binding.bottomNavigation.setOnItemSelectedListener { + viewModel.setMainTab(MainPage.NULL) + when (it.itemId) { R.id.menuHome -> { - showFragment(HomeFragment.newIntent(), HomeFragment.TAG) + viewModel.mainEvent(MainViewModel.MainEvent.NavigateToHome) return@setOnItemSelectedListener true } R.id.menuSkin -> { - showFragment(SkinFragment.newIntent(), SkinFragment.TAG) + viewModel.mainEvent(MainViewModel.MainEvent.NavigateToSkin) return@setOnItemSelectedListener true } R.id.menuSocial -> { - showFragment(SocialFragment(), SocialFragment.TAG) + viewModel.mainEvent(MainViewModel.MainEvent.NavigateToSocial) return@setOnItemSelectedListener true } R.id.menuMyPage -> { - showFragment(MyPageFragment.newIntent(), MyPageFragment.TAG) + viewModel.mainEvent(MainViewModel.MainEvent.NavigateToMyPage) return@setOnItemSelectedListener true } @@ -126,22 +347,67 @@ class MainActivity : BaseActivity(R.layout.activi // 현재 포그라운드에서만 이동 됨 - 서버와 얘기해야함 private fun handleIntent(intent: Intent) { val destination = intent.getStringExtra("fragmentDestination") + Log.d("알림", destination.toString()) when (destination) { + MyFirebaseMessagingService.FragmentDestination.HOME_FRAGMENT.name -> { + viewModel.setMainTab(MainPage.HOME) + } + MyFirebaseMessagingService.FragmentDestination.SKIN_FRAGMENT.name -> { - showFragment(SkinFragment.newIntent(), SkinFragment.TAG) + viewModel.setMainTab(MainPage.SKIN) + } + + MyFirebaseMessagingService.FragmentDestination.FRIEND_REQUEST_ACTIVITY.name -> { + startActivity(FriendAcceptActivity.newIntent(this, FriendAcceptActivity.FRIEND)) } + + MyFirebaseMessagingService.FragmentDestination.FRIEND_ACCEPT_ACTIVITY.name -> { + startActivity(FriendActivity.newIntent(this, FriendActivity.FRIEND)) + } + else -> { } } } + + override fun onResume() { + super.onResume() + if (!isPermissionRequested) { + isPermissionRequested = true + essentialPermissionLauncher.launch(essentialPermissionList) + } else { + if (essentialPermissionList.any { + ActivityCompat.checkSelfPermission( + this, + it + ) != PackageManager.PERMISSION_GRANTED + }) { + isPermissionRequested = false + } + } + } + + companion object { fun goMain(context: Context) { val intent = Intent(context, MainActivity::class.java) + intent.putExtra( + "fragmentDestination", + MyFirebaseMessagingService.FragmentDestination.HOME_FRAGMENT.name + ) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) } } + enum class MainPage { + HOME, + SKIN, + AR, + SOCIAL, + MY_PAGE, + NULL, + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainViewModel.kt new file mode 100644 index 000000000..dc860e997 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainViewModel.kt @@ -0,0 +1,22 @@ +package com.droidblossom.archive.presentation.ui + +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface MainViewModel { + + val mainEvents: SharedFlow + val selectedMainTab: StateFlow + + fun setMainTab(selectedTab : MainActivity.MainPage) + fun mainEvent(event: MainEvent) + + sealed class MainEvent { + object NavigateToCamera : MainEvent() + object NavigateToHome : MainEvent() + object NavigateToMyPage : MainEvent() + object NavigateToSocial : MainEvent() + object NavigateToSkin : MainEvent() + data class ShowToastMessage(val message: String) : MainEvent() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainViewModelImpl.kt new file mode 100644 index 000000000..2bfa51cee --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/MainViewModelImpl.kt @@ -0,0 +1,48 @@ +package com.droidblossom.archive.presentation.ui + +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.util.DataStoreUtils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MainViewModelImpl @Inject constructor( + private val dataStoreUtils: DataStoreUtils +): BaseViewModel(), MainViewModel { + + + private val _mainEvents = MutableSharedFlow() + override val mainEvents: SharedFlow + get() = _mainEvents.asSharedFlow() + + private val _selectedMainTab = MutableStateFlow(null) + override val selectedMainTab: StateFlow + get() = _selectedMainTab + + init { + viewModelScope.launch { + val selectedTabId = dataStoreUtils.fetchSelectedTab() + val selectedTab = MainActivity.MainPage.values().find { it.name == selectedTabId } + _selectedMainTab.value = selectedTab ?: MainActivity.MainPage.HOME + } + } + + override fun setMainTab(selectedTab : MainActivity.MainPage) { + _selectedMainTab.value = selectedTab + } + + override fun mainEvent(event: MainViewModel.MainEvent) { + viewModelScope.launch { + _mainEvents.emit(event) + } + } + + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/AuthViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/AuthViewModelImpl.kt index 0d1177505..70fe88fbc 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/AuthViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/AuthViewModelImpl.kt @@ -56,10 +56,9 @@ class AuthViewModelImpl @Inject constructor( private val _signUpEvents = MutableSharedFlow() override val signUpEvents: SharedFlow = _signUpEvents.asSharedFlow() - private val _phoneNumber = MutableStateFlow("") - override val phoneNumber: MutableStateFlow = _phoneNumber + override val phoneNumber = MutableStateFlow("") - private val _rawPhoneNumber = _phoneNumber + private val _rawPhoneNumber = phoneNumber .map { it.replace("-", "") } .stateIn(viewModelScope, SharingStarted.Eagerly, "") override val rawPhoneNumber: StateFlow = _rawPhoneNumber @@ -104,7 +103,7 @@ class AuthViewModelImpl @Inject constructor( certificationNumber3.value = "" certificationNumber2.value = "" certificationNumber1.value = "" - _phoneNumber.value = "" + phoneNumber.value = "" } override fun memberStatusCheck(memberStatusCheckData : CheckStatus, signUpData : SignUp){ @@ -193,7 +192,7 @@ class AuthViewModelImpl @Inject constructor( override fun clearPhoneNumber(){ viewModelScope.launch { - _phoneNumber.emit("") + phoneNumber.emit("") } } @@ -241,7 +240,9 @@ class AuthViewModelImpl @Inject constructor( override fun submitCertificationNumber(){ viewModelScope.launch { - validMessageUseCase(VerificationNumberValid(certificationNumber.value, rawPhoneNumber.value).toDto()).collect{ result -> + validMessageUseCase( + VerificationNumberValid(certificationNumber.value, rawPhoneNumber.value).toDto() + ).collect{ result -> result.onSuccess { dataStoreUtils.saveAccessToken(it.accessToken) dataStoreUtils.saveRefreshToken(it.refreshToken) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignInFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignInFragment.kt index fa363e0eb..3812888ce 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignInFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignInFragment.kt @@ -1,5 +1,6 @@ package com.droidblossom.archive.presentation.ui.auth +import android.Manifest import android.app.Activity import android.os.Bundle import android.view.View @@ -15,6 +16,8 @@ import com.droidblossom.archive.databinding.FragmentSignInBinding import com.droidblossom.archive.domain.model.auth.SignUp import com.droidblossom.archive.domain.model.member.CheckStatus import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment import com.droidblossom.archive.presentation.ui.MainActivity import com.droidblossom.archive.util.SocialLoginUtil import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -38,6 +41,43 @@ class SignInFragment : BaseFragment(R.l } } + private val essentialPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + MainActivity.goMain(requireContext()) + } + + else -> { + handleEssentialPermissionsDenied() + } + } + + } + + private fun handleEssentialPermissionsDenied() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) || shouldShowRequestPermissionRationale( + Manifest.permission.ACCESS_COARSE_LOCATION + ) || shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + ) { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + } else { + showSettingsDialog( + PermissionDialogFragment.PermissionType.ESSENTIAL, + object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + requireActivity().finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings{essentialPermissionLauncher.launch(essentialPermissionList)} + } + + }) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -71,7 +111,7 @@ class SignInFragment : BaseFragment(R.l when (event) { is AuthViewModel.SignInEvent.NavigateToMain -> { - MainActivity.goMain(requireContext()) + essentialPermissionLauncher.launch(essentialPermissionList) } is AuthViewModel.SignInEvent.NavigateToSignUp -> { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignUpSuccessFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignUpSuccessFragment.kt index 5e9f389df..937cac1d3 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignUpSuccessFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/auth/SignUpSuccessFragment.kt @@ -1,13 +1,17 @@ package com.droidblossom.archive.presentation.ui.auth +import android.Manifest import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.activityViewModels import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentSignUpSuccessBinding import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment import com.droidblossom.archive.presentation.ui.MainActivity import dagger.hilt.android.AndroidEntryPoint @@ -20,7 +24,45 @@ class SignUpSuccessFragment : BaseFragment + when { + permissions.all { it.value } -> { + MainActivity.goMain(requireContext()) + } + + else -> { + handleEssentialPermissionsDenied() + } + } + + } + + private fun handleEssentialPermissionsDenied() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) || shouldShowRequestPermissionRationale( + Manifest.permission.ACCESS_COARSE_LOCATION + ) || shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + ) { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + } else { + showSettingsDialog( + PermissionDialogFragment.PermissionType.ESSENTIAL, + object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + requireActivity().finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings{essentialPermissionLauncher.launch(essentialPermissionList)} + } + + }) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/ARContentNode.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/ARContentNode.kt index 09b0b25d8..6272cb360 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/ARContentNode.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/ARContentNode.kt @@ -2,10 +2,11 @@ package com.droidblossom.archive.presentation.ui.camera import android.content.Context import android.view.LayoutInflater +import androidx.fragment.app.DialogFragment import com.bumptech.glide.Glide import com.droidblossom.archive.R import com.droidblossom.archive.databinding.ItemCapsuleSkinBinding -import com.droidblossom.archive.domain.model.common.CapsuleMarker +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchor import com.droidblossom.archive.presentation.ui.home.dialog.CapsulePreviewDialogFragment import com.droidblossom.archive.util.FragmentManagerProvider import com.google.ar.sceneform.rendering.ViewAttachmentManager @@ -18,7 +19,7 @@ class ARContentNode( val arscene: ARSceneView, val viewAttManager: ViewAttachmentManager, val fragmentManagerProvider: FragmentManagerProvider, - val capsule: CapsuleMarker, + val capsule: CapsuleAnchor, val layoutInflater: LayoutInflater, val context: Context, val onLoaded: (node: ViewNode) -> Unit @@ -53,9 +54,17 @@ class ARContentNode( isEditable = true } onSingleTapConfirmed = { - val sheet = CapsulePreviewDialogFragment.newInstance(capsule.id.toString(), capsule.capsuleType.toString(), true) - sheet.show(fragmentManagerProvider.provideFragmentManager(), "CapsulePreviewDialog") - false + val existingDialog = fragmentManagerProvider.provideFragmentManager().findFragmentByTag(CapsulePreviewDialogFragment.TAG) as DialogFragment? + if (existingDialog == null) { + val dialog = CapsulePreviewDialogFragment.newInstance( + "-1", + capsule.id.toString(), + capsule.capsuleType.toString(), + true + ) + dialog.show(fragmentManagerProvider.provideFragmentManager(), CapsulePreviewDialogFragment.TAG) + } + true } } onLoaded(viewNode) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraFragment.kt index cc0da57d2..18c33987a 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraFragment.kt @@ -1,20 +1,30 @@ package com.droidblossom.archive.presentation.ui.camera import android.Manifest +import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import android.view.View -import androidx.core.content.ContentProviderCompat.requireContext +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityCompat +import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentCameraBinding -import com.droidblossom.archive.domain.model.common.CapsuleMarker +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchor import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment +import com.droidblossom.archive.presentation.ui.MainActivity import com.droidblossom.archive.presentation.ui.home.dialog.CapsulePreviewDialogFragment +import com.droidblossom.archive.util.CustomLifecycleOwner import com.droidblossom.archive.util.FragmentManagerProvider import com.droidblossom.archive.util.LocationUtil import com.google.ar.core.Anchor @@ -25,6 +35,7 @@ import com.google.ar.sceneform.rendering.ViewAttachmentManager import dagger.hilt.android.AndroidEntryPoint import io.github.sceneview.ar.ARSceneView import io.github.sceneview.ar.node.AnchorNode +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @AndroidEntryPoint @@ -33,30 +44,152 @@ class CameraFragment : FragmentManagerProvider { override val viewModel: CameraViewModelImpl by viewModels() - - // private val capsules: MutableList = mutableListOf() lateinit var arSceneView: ARSceneView - private var anchorNode: AnchorNode? = null private lateinit var session: Session private lateinit var config: Config private lateinit var viewAttachmentManager: ViewAttachmentManager + private val locationUtil by lazy { LocationUtil(requireContext()) } + + private val visibleLifecycleOwner: CustomLifecycleOwner by lazy { + CustomLifecycleOwner() + } + + private val arPermissionList = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + locationUtil.getCurrentLocation { latitude, longitude -> + viewModel.getCapsules(latitude = latitude, longitude = longitude) + } + } + + permissions.none { it.value } -> { + handleAllPermissionsDenied() + } + + else -> { + handlePartialPermissionsDenied(permissions) + } + } + } + + private val requestCameraPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + viewAttachmentManager.onResume() + if (this.isHidden) { + onHidden() + } + requestPermissionLauncher.launch(arPermissionList) + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showToastMessage("AR 기능을 사용하려면 카메라 권한이 필요합니다.") + } else { + requestPermissionLauncher.launch(arPermissionList) + } + + } + } + + private fun handleAllPermissionsDenied() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + ) { + showToastMessage("AR 기능을 사용하려면 카메라, 위치 권한이 필요합니다.") + } else { + showSettingsDialog( + PermissionDialogFragment.PermissionType.AR, + object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("AR 기능을 사용하려면 카메라, 위치 권한이 필요합니다.") + //requireActivity().finish() + + } + + override fun onRightButtonClicked() { + navigateToAppSettings { requestPermissionLauncher.launch(arPermissionList) } + } + + }) + } + } + + private fun handlePartialPermissionsDenied(permissions: Map) { + permissions.forEach { (permission, granted) -> + if (!granted) { + when (permission) { + Manifest.permission.CAMERA -> showPermissionDialog(PermissionDialogFragment.PermissionType.CAMERA) + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION -> { + if (!permissions.getValue(Manifest.permission.ACCESS_FINE_LOCATION) || + !permissions.getValue(Manifest.permission.ACCESS_COARSE_LOCATION) + ) { + showPermissionDialog(PermissionDialogFragment.PermissionType.LOCATION) + } + } + } + } + } + } + + private fun showPermissionDialog(permissionType: PermissionDialogFragment.PermissionType) { + + if (shouldShowRequestPermissionRationale(permissionType.toString())) { + showToastMessage("AR 기능을 사용하려면 ${permissionType.description} 권한이 필요합니다.") + } else { + showSettingsDialog(permissionType, object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("AR 기능을 사용하려면 ${permissionType.description} 권한이 필요합니다.") + //requireActivity().finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings { requestPermissionLauncher.launch(arPermissionList) } + } + + }) + } + + } + override fun provideFragmentManager(): FragmentManager { return parentFragmentManager } override fun observeData() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.cameraEvents.collect { event -> when (event) { is CameraViewModel.CameraEvent.ShowCapsulePreviewDialog -> { - val sheet = CapsulePreviewDialogFragment.newInstance( - event.capsuleId, - event.capsuleType, - true - ) - sheet.show(parentFragmentManager, "CapsulePreviewDialog") + + val existingDialog = parentFragmentManager.findFragmentByTag(CapsulePreviewDialogFragment.TAG) as DialogFragment? + if (existingDialog == null) { + val dialog = CapsulePreviewDialogFragment.newInstance( + "-1", + event.capsuleId, + event.capsuleType, + true + ) + dialog.show(parentFragmentManager, CapsulePreviewDialogFragment.TAG) + } + + } + + is CameraViewModel.CameraEvent.ShowLoading -> { + showLoading(requireContext()) + } + + is CameraViewModel.CameraEvent.DismissLoading -> { + dismissLoading() } else -> {} @@ -65,11 +198,24 @@ class CameraFragment : } } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.anchorNodes + .filter { anchorNodes -> + anchorNodes.size == viewModel.capsuleListSize + } + .collect { + dismissLoading() + showToastMessage("${it.size}개의 캡슐을 찾았습니다.") + } + } + } + + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.capsuleList.collect { capsuleList -> arSceneView.onSessionUpdated = { session, frame -> - if (anchorNode == null) { + if (viewModel.capsuleList.value.isNotEmpty() && !viewModel.isCapsulesAdded) { Log.d("CameraFragmentAR", "earth setting start") val earth = session.earth if (earth == null) { @@ -96,6 +242,7 @@ class CameraFragment : ) addAnchorNode(earthAnchor, capsule) } + viewModel.isCapsulesAdded = true } } } @@ -104,27 +251,45 @@ class CameraFragment : } } } - } - val permissionList = arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION - ) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val locationUtil = LocationUtil(requireContext()) - locationUtil.getCurrentLocation { latitude, longitude -> - viewModel.getCapsules(latitude = latitude, longitude = longitude) - } + showLoading(requireContext()) + binding.vm = viewModel + binding.view = this arSceneView = binding.sceneView viewAttachmentManager = ViewAttachmentManager( arSceneView.context, arSceneView ) - initView() - createSession() + + val layoutParams = binding.filterAll.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.filterAll.layoutParams = layoutParams + + initCustomLifeCycle() + + if (ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + initView() + createSession() + } else { + MainActivity.goMain(requireContext()) + } } private fun initView() { @@ -132,37 +297,38 @@ class CameraFragment : config.geospatialMode = Config.GeospatialMode.ENABLED config.planeFindingMode = Config.PlaneFindingMode.DISABLED } + } - private fun addAnchorNode(anchor: Anchor, capsule: CapsuleMarker) { - Log.d("CameraFragmentAR", "addAnchorNode added") - val arContentNode = - arSceneView.let { scenview -> - viewAttachmentManager.let { attachManager -> - ARContentNode( - scenview, - attachManager, - this, - capsule, - layoutInflater, - requireContext(), - onLoaded = { viewNode -> - arSceneView.engine.let { - AnchorNode(it, anchor) - .apply { - isEditable = true - lifecycleScope.launch { - addChildNode(viewNode) - } - anchorNode = this - } - }.let { - arSceneView.addChildNode(it) + private fun addAnchorNode(anchor: Anchor, capsule: CapsuleAnchor) { + Log.d("CameraFragmentAR", "${capsule.id} addAnchorNode added") + arSceneView.let { sceneView -> + viewAttachmentManager.let { attachManager -> + ARContentNode( + sceneView, + attachManager, + this, + capsule, + layoutInflater, + requireContext(), + onLoaded = { viewNode -> + sceneView.engine.let { + AnchorNode(it, anchor).apply { + isEditable = true + lifecycleScope.launch { + addChildNode(viewNode) + } + viewModel.addAnchorNode(this) } - }) - } + }.let { + sceneView.addChildNode(it) + } + } + ) } + } + } private fun createSession() { @@ -172,23 +338,75 @@ class CameraFragment : session.configure(config) } + private fun initCustomLifeCycle() { + viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_START, + Lifecycle.Event.ON_CREATE, + Lifecycle.Event.ON_RESUME, + Lifecycle.Event.ON_PAUSE, -> { + if (isHidden) { + visibleLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } else { + visibleLifecycleOwner.handleLifecycleEvent(event) + } + } + else -> { + visibleLifecycleOwner.handleLifecycleEvent(event) + } + } + } + }) + } + override fun onResume() { super.onResume() - viewAttachmentManager.onResume() + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) if (hidden) { + onHidden() + } else { + onShow() + } + } + + private fun onHidden() { + dismissLoading() + if (ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) { + arSceneView.clearChildNodes() + viewModel.clearAnchorNode() viewAttachmentManager.onPause() arSceneView.session?.pause() - } else { + } + } + + private fun onShow() { + visibleLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START) + if (ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) { arSceneView.session?.resume() viewAttachmentManager.onResume() - val locationUtil = LocationUtil(requireContext()) - locationUtil.getCurrentLocation { latitude, longitude -> - viewModel.getCapsules(latitude = latitude, longitude = longitude) - } + //requestPermissionLauncher.launch(arPermissionList) + } + } + + fun onClickFilter(capsuleFilterType: CameraViewModel.CapsuleFilterType) { + showLoading(requireContext()) + arSceneView.clearChildNodes() + viewModel.clearAnchorNode() + locationUtil.getCurrentLocation { latitude, longitude -> + viewModel.selectFilter(capsuleFilterType, latitude = latitude, longitude = longitude) } } @@ -197,4 +415,5 @@ class CameraFragment : const val TAG = "CAMERA" fun newIntent() = CameraFragment() } + } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModel.kt index c9358fd54..ee0a89625 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModel.kt @@ -1,18 +1,41 @@ package com.droidblossom.archive.presentation.ui.camera -import com.droidblossom.archive.domain.model.common.CapsuleMarker +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchor +import io.github.sceneview.ar.node.AnchorNode import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow interface CameraViewModel { - val capsuleList:StateFlow> + val selectedCapsuleFilter : StateFlow + val capsuleList:StateFlow> val cameraEvents: SharedFlow + var capsuleListSize: Int + val anchorNodes: StateFlow> + val isFriendsCapsuleDisplay : StateFlow + val isCapsulesAdded: Boolean + + fun selectFilter(capsuleFilterType: CapsuleFilterType, latitude: Double, longitude: Double) + fun getCapsules(latitude: Double, longitude: Double) - fun getCapsules(latitude: Double, longitude: Double,): List fun cameraEvent(event : CameraEvent) + fun addAnchorNode(anchorNode: AnchorNode) + + fun clearAnchorNode() + + sealed class CameraEvent{ data class ShowCapsulePreviewDialog(val capsuleId: String, val capsuleType: String) : CameraEvent() + object ShowLoading : CameraEvent() + object DismissLoading : CameraEvent() + } + + enum class CapsuleFilterType(val description: String){ + FILTER_ALL("ALL"), + FILTER_SECRET("SECRET"), + FILTER_GROUP("GROUP"), + FILTER_PUBLIC_MY("PUBLIC"), + FILTER_PUBLIC_FRIEND("PUBLIC_FRIEND") } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModelImpl.kt index 9e5a8a726..060c2edbe 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/camera/CameraViewModelImpl.kt @@ -1,48 +1,130 @@ package com.droidblossom.archive.presentation.ui.camera +import android.util.Log import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.domain.model.common.CapsuleMarker -import com.droidblossom.archive.domain.usecase.capsule.NearbyCapsulesUseCase +import com.droidblossom.archive.domain.model.capsule.CapsuleAnchor +import com.droidblossom.archive.domain.usecase.capsule.NearbyFriendsCapsulesARUseCase +import com.droidblossom.archive.domain.usecase.capsule.NearbyMyCapsulesARUseCase import com.droidblossom.archive.presentation.base.BaseViewModel import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sceneview.ar.node.AnchorNode import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class CameraViewModelImpl@Inject constructor( - private val nearbyCapsulesUseCase: NearbyCapsulesUseCase + private val nearbyMyCapsulesARUseCase: NearbyMyCapsulesARUseCase, + private val nearbyFriendsCapsulesARUseCase: NearbyFriendsCapsulesARUseCase ) : BaseViewModel(), CameraViewModel { private val _cameraEvents = MutableSharedFlow() override val cameraEvents: SharedFlow get() = _cameraEvents.asSharedFlow() - private val _capsuleList = MutableStateFlow(listOf()) - override val capsuleList: StateFlow> + private val _capsuleList = MutableStateFlow(listOf()) + override val capsuleList: StateFlow> get() = _capsuleList + override var capsuleListSize = -1 + + private val _anchorNodes = MutableStateFlow>(mutableListOf()) + override val anchorNodes get() = _anchorNodes + + private val _isFriendsCapsuleDisplay = MutableStateFlow(false) + override val isFriendsCapsuleDisplay: StateFlow + get() = _isFriendsCapsuleDisplay + + private val _selectedCapsuleFilter = MutableStateFlow(CameraViewModel.CapsuleFilterType.FILTER_ALL) + override val selectedCapsuleFilter: StateFlow + get() = _selectedCapsuleFilter + override var isCapsulesAdded = false + + + override fun addAnchorNode(anchorNode: AnchorNode) { + val updatedList = _anchorNodes.value.toMutableList() + updatedList.add(anchorNode) + _anchorNodes.value = updatedList + } + override fun clearAnchorNode() { + isCapsulesAdded = false + capsuleListSize = -1 + _capsuleList.value = mutableListOf() + _anchorNodes.value = mutableListOf() + capsuleListSize = 0 + } + + override fun cameraEvent(event: CameraViewModel.CameraEvent) { viewModelScope.launch { _cameraEvents.emit(event) } } - override fun getCapsules(latitude: Double, longitude: Double) : List { + + override fun getCapsules(latitude: Double, longitude: Double){ + when(selectedCapsuleFilter.value){ + CameraViewModel.CapsuleFilterType.FILTER_ALL -> { + getMyCapsules(latitude, longitude) + } + CameraViewModel.CapsuleFilterType.FILTER_SECRET -> { + getMyCapsules(latitude, longitude) + } + CameraViewModel.CapsuleFilterType.FILTER_GROUP -> { + getMyCapsules(latitude, longitude) + } + CameraViewModel.CapsuleFilterType.FILTER_PUBLIC_MY -> { + getMyCapsules(latitude, longitude) + } + CameraViewModel.CapsuleFilterType.FILTER_PUBLIC_FRIEND -> { + getFriendsCapsules(latitude, longitude) + } + } + + } + override fun selectFilter(capsuleFilterType: CameraViewModel.CapsuleFilterType, latitude: Double, longitude: Double){ + _selectedCapsuleFilter.value = capsuleFilterType + getCapsules(latitude, longitude) + } + + + private fun getMyCapsules(latitude: Double, longitude: Double){ viewModelScope.launch { - nearbyCapsulesUseCase(latitude,longitude,1.0,"ALL").collect{ result-> + nearbyMyCapsulesARUseCase(latitude,longitude,0.1,selectedCapsuleFilter.value.description).collect{ result-> result.onSuccess { - _capsuleList.emit(it.capsules) + capsuleListSize = it.capsuleAnchors.size + _capsuleList.value = it.capsuleAnchors + if (capsuleList.value.isEmpty()){ + cameraEvent(CameraViewModel.CameraEvent.DismissLoading) + capsuleListSize = 0 + } }.onFail { - + cameraEvent(CameraViewModel.CameraEvent.DismissLoading) } } } - return capsuleList.value } + private fun getFriendsCapsules(latitude: Double, longitude: Double){ + viewModelScope.launch { + nearbyFriendsCapsulesARUseCase(latitude,longitude,0.1).collect{ result-> + result.onSuccess { + capsuleListSize = it.capsuleAnchors.size + _capsuleList.value = it.capsuleAnchors + if (capsuleList.value.isEmpty()) { + cameraEvent(CameraViewModel.CameraEvent.DismissLoading) + capsuleListSize = 0 + } + }.onFail { + cameraEvent(CameraViewModel.CameraEvent.DismissLoading) + } + } + } + } + } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailActivity.kt index 1399abe02..6d2a534c3 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailActivity.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailActivity.kt @@ -3,13 +3,19 @@ package com.droidblossom.archive.presentation.ui.capsule import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.PopupWindow import androidx.activity.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.droidblossom.archive.R import com.droidblossom.archive.databinding.ActivityCapsuleDetailBinding +import com.droidblossom.archive.databinding.PopupMenuCapsuleBinding import com.droidblossom.archive.domain.model.common.ContentType import com.droidblossom.archive.domain.model.common.ContentUrl import com.droidblossom.archive.presentation.base.BaseActivity @@ -66,7 +72,7 @@ class CapsuleDetailActivity : } HomeFragment.CapsuleType.PUBLIC -> { - + viewModel.getPublicCapsuleDetail(capsuleInd) } null -> {} @@ -74,6 +80,9 @@ class CapsuleDetailActivity : binding.closeBtn.setOnClickListener { finish() } + binding.capsuleMenuImg.setOnClickListener { view -> + showPopupMenu(view) + } } private fun initRVA() { @@ -83,6 +92,34 @@ class CapsuleDetailActivity : } + private fun showPopupMenu(view: View) { + val popupMenuBinding = PopupMenuCapsuleBinding.inflate(LayoutInflater.from(this), null, false) + + val density = this.resources.displayMetrics.density + val widthPixels = (120 * density).toInt() + + val popupWindow = PopupWindow(popupMenuBinding.root, + widthPixels, + LinearLayout.LayoutParams.WRAP_CONTENT, + true) + + popupWindow.contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val popupWidth = popupWindow.contentView.measuredWidth + + val xOffset = (view.width / 2) - (popupWidth / 2) - 213 + popupWindow.showAsDropDown(view, xOffset, -view.height) + + popupMenuBinding.menuMap.setOnClickListener { + popupWindow.dismiss() + } + popupMenuBinding.menuModify.setOnClickListener { + popupWindow.dismiss() + } + popupMenuBinding.menuDelete.setOnClickListener { + popupWindow.dismiss() + } + } + override fun observeData() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModel.kt index 6c479ec7b..a7d72262c 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModel.kt @@ -1,8 +1,6 @@ package com.droidblossom.archive.presentation.ui.capsule -import com.droidblossom.archive.domain.model.common.ContentUrl -import com.droidblossom.archive.domain.model.secret.SecretCapsuleDetail -import com.droidblossom.archive.presentation.ui.mypage.MyPageViewModel +import com.droidblossom.archive.domain.model.common.CapsuleDetail import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -10,8 +8,9 @@ interface CapsuleDetailViewModel { val detailEvents : SharedFlow - val capsuleDetail :StateFlow + val capsuleDetail :StateFlow fun getSecretCapsuleDetail(id:Long) + fun getPublicCapsuleDetail(id:Long) sealed class DetailEvent { data class ShowToastMessage(val message : String) : DetailEvent() diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModelImpl.kt index b10613f59..1778a8a90 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/capsule/CapsuleDetailViewModelImpl.kt @@ -2,15 +2,13 @@ package com.droidblossom.archive.presentation.ui.capsule import android.util.Log import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.domain.model.common.ContentType -import com.droidblossom.archive.domain.model.common.ContentUrl -import com.droidblossom.archive.domain.model.secret.SecretCapsuleDetail +import com.droidblossom.archive.domain.model.common.CapsuleDetail +import com.droidblossom.archive.domain.usecase.open.PublicCapsuleDetailUseCase import com.droidblossom.archive.domain.usecase.secret.SecretCapsuleDetailUseCase import com.droidblossom.archive.presentation.base.BaseViewModel import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -21,25 +19,40 @@ import javax.inject.Inject @HiltViewModel class CapsuleDetailViewModelImpl @Inject constructor( - private val secretCapsuleDetailUseCase: SecretCapsuleDetailUseCase -): BaseViewModel(), CapsuleDetailViewModel { + private val secretCapsuleDetailUseCase: SecretCapsuleDetailUseCase, + private val publicCapsuleDetailUseCase: PublicCapsuleDetailUseCase +) : BaseViewModel(), CapsuleDetailViewModel { private val _detailEvent = MutableSharedFlow() override val detailEvents: SharedFlow - get() = _detailEvent.asSharedFlow() + get() = _detailEvent.asSharedFlow() - private val _capsuleDetail = MutableStateFlow(SecretCapsuleDetail()) - override val capsuleDetail: StateFlow + private val _capsuleDetail = MutableStateFlow(CapsuleDetail()) + override val capsuleDetail: StateFlow get() = _capsuleDetail override fun getSecretCapsuleDetail(id: Long) { viewModelScope.launch { - secretCapsuleDetailUseCase(id).collect{ result -> + secretCapsuleDetailUseCase(id).collect { result -> result.onSuccess { detail -> - Log.d("디테일","${detail}") + Log.d("디테일", "${detail}") _capsuleDetail.emit(detail) - }.onFail { - Log.d("디테일","${it}") + }.onFail { + Log.d("디테일", "${it}") + _detailEvent.emit(CapsuleDetailViewModel.DetailEvent.ShowToastMessage("상세정보 불러오기 실패")) + } + } + } + } + + override fun getPublicCapsuleDetail(id: Long) { + viewModelScope.launch { + publicCapsuleDetailUseCase(id).collect { result -> + result.onSuccess { detail -> + Log.d("디테일", "${detail}") + _capsuleDetail.emit(detail) + }.onFail { + Log.d("디테일", "${it}") _detailEvent.emit(CapsuleDetailViewModel.DetailEvent.ShowToastMessage("상세정보 불러오기 실패")) } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/CapsuleClusteringKey.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/CapsuleClusteringKey.kt new file mode 100644 index 000000000..eeebefbad --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/CapsuleClusteringKey.kt @@ -0,0 +1,18 @@ +package com.droidblossom.archive.presentation.ui.home + +import com.naver.maps.geometry.LatLng +import com.naver.maps.map.clustering.ClusteringKey + +class CapsuleClusteringKey(val id: Long, val capsuleType: HomeFragment.CapsuleType, private val position : LatLng) : ClusteringKey { + + override fun getPosition() = position + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val itemKey = other as CapsuleClusteringKey + return id == itemKey.id + } + + override fun hashCode() = id.hashCode() +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeFragment.kt index bb3a57d0a..b2a7d0163 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeFragment.kt @@ -1,45 +1,43 @@ package com.droidblossom.archive.presentation.ui.home -import android.graphics.Point -import android.location.Location +import android.graphics.Color import android.os.Bundle -import android.util.Log import android.view.View import android.view.ViewGroup -import androidx.core.content.ContentProviderCompat.requireContext import androidx.core.content.ContextCompat -import androidx.core.graphics.toPointF +import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentHomeBinding -import com.droidblossom.archive.domain.model.common.CapsuleMarker +import com.droidblossom.archive.domain.model.capsule.CapsuleMarker import com.droidblossom.archive.presentation.base.BaseFragment -import com.droidblossom.archive.presentation.snack.HomeSnackBarBig -import com.droidblossom.archive.presentation.snack.HomeSnackBarSmall import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleActivity import com.droidblossom.archive.presentation.ui.home.dialog.CapsulePreviewDialogFragment -import com.droidblossom.archive.util.CapsuleTypeUtils +import com.droidblossom.archive.presentation.ui.home.notification.NotificationActivity import com.droidblossom.archive.util.LocationUtil import com.naver.maps.geometry.LatLng +import com.naver.maps.geometry.LatLngBounds import com.naver.maps.map.CameraUpdate import com.naver.maps.map.LocationTrackingMode import com.naver.maps.map.MapFragment import com.naver.maps.map.NaverMap import com.naver.maps.map.OnMapReadyCallback -import com.naver.maps.map.Projection +import com.naver.maps.map.clustering.ClusterMarkerInfo +import com.naver.maps.map.clustering.Clusterer +import com.naver.maps.map.clustering.DefaultClusterMarkerUpdater +import com.naver.maps.map.clustering.DefaultLeafMarkerUpdater +import com.naver.maps.map.clustering.LeafMarkerInfo import com.naver.maps.map.overlay.Marker import com.naver.maps.map.overlay.Overlay import com.naver.maps.map.overlay.OverlayImage import com.naver.maps.map.util.FusedLocationSource +import com.naver.maps.map.util.MarkerIcons import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.random.Random +import kotlin.math.pow @AndroidEntryPoint class HomeFragment : BaseFragment(R.layout.fragment_home), @@ -47,10 +45,61 @@ class HomeFragment : BaseFragment(R.layo override val viewModel: HomeViewModelImpl by viewModels() - private lateinit var naverMap: NaverMap + //private lateinit var naverMap: NaverMap private lateinit var locationUtil: LocationUtil private lateinit var locationSource: FusedLocationSource - private val markers: MutableList = mutableListOf() + + // https://navermaps.github.io/android-map-sdk/guide-ko/5-8.html + private val clusterer: Clusterer = + Clusterer.Builder() + .clusterMarkerUpdater(object : DefaultClusterMarkerUpdater() { + override fun updateClusterMarker(info: ClusterMarkerInfo, marker: Marker) { + super.updateClusterMarker(info, marker) + marker.icon = OverlayImage.fromResource(R.drawable.ic_cluster_marker_46) + marker.captionColor = Color.BLACK + marker.captionHaloColor = + ContextCompat.getColor(requireContext(), R.color.main_bg_1) + marker.captionTextSize = if (info.size >= 100) 15f else 18f + marker.onClickListener = Overlay.OnClickListener { + // 클러스터된 거 클릭이벤트 - 나중에 클러스터에 행당된 캡슐들 사이드에 보여주거나 하면 좋을듯? + true + } + } + }).leafMarkerUpdater(object : DefaultLeafMarkerUpdater() { + override fun updateLeafMarker(info: LeafMarkerInfo, marker: Marker) { + super.updateLeafMarker(info, marker) + val key = info.key as CapsuleClusteringKey + marker.icon = when (key.capsuleType) { + CapsuleType.SECRET -> OverlayImage.fromResource(R.drawable.ic_marker_pin_secret) + CapsuleType.GROUP -> OverlayImage.fromResource(R.drawable.ic_marker_pin_group) + CapsuleType.PUBLIC -> OverlayImage.fromResource(R.drawable.ic_marker_pin_public) + } + marker.onClickListener = Overlay.OnClickListener { + viewModel.homeEvent( + HomeViewModel.HomeEvent.ShowCapsulePreviewDialog( + key.id.toString(), + key.capsuleType.toString() + ) + ) + true + } + } + }) + .minZoom(FIXZOOM.toInt() - 6) + .maxZoom(FIXZOOM.toInt() + 2) + .build() + + + private val zoomToRadiusMap: Map by lazy { + val map = mutableMapOf() + + for (zoomLevel in MINZOOM.toInt()..MAXZOOM.toInt()) { + val normalizedZoom = 1 - ((zoomLevel - MINZOOM) / (MAXZOOM - MINZOOM)) + val radius = MINRADIUS + (MAXRADIUS - MINRADIUS) * normalizedZoom.pow(3) + map[zoomLevel.toDouble()] = radius + } + map.toMap() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -76,24 +125,10 @@ class HomeFragment : BaseFragment(R.layo makeSecretCapsuleBtn.setOnClickListener { startActivity(CreateCapsuleActivity.newIntent(requireContext(), 3)) } - snackbarTestBtn.setOnClickListener { - HomeSnackBarSmall(requireView()).show() - } - snackbarBigText.setOnClickListener { - HomeSnackBarBig(requireView(), "", "").show() - } refreshBtn.setOnClickListener { - locationUtil.getCurrentLocation { latitude, longitude -> - viewModel.getNearbyCapsules( - latitude, - longitude, - calculateRadiusForZoomLevel(), - viewModel.filterCapsuleSelect.value.toString() - ) - } + fetchCapsulesInCameraFocus() } - } } @@ -103,16 +138,7 @@ class HomeFragment : BaseFragment(R.layo repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.filterCapsuleSelect.collect { viewModel.resetNearbyCapsules() - locationUtil.getCurrentLocation { latitude, longitude -> - if (::naverMap.isInitialized) { - viewModel.getNearbyCapsules( - latitude, - longitude, - calculateRadiusForZoomLevel(), - viewModel.filterCapsuleSelect.value.toString() - ) - } - } + fetchCapsulesInCameraFocus() } } } @@ -121,46 +147,86 @@ class HomeFragment : BaseFragment(R.layo repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.homeEvents.collect { event -> when (event) { + HomeViewModel.HomeEvent.GoNotification -> { + startActivity(NotificationActivity.newIntent(requireContext())) + } is HomeViewModel.HomeEvent.ShowCapsulePreviewDialog -> { - val sheet = CapsulePreviewDialogFragment.newInstance(event.capsuleId, event.capsuleType, false) - sheet.show(parentFragmentManager, "CapsulePreviewDialog") + val existingDialog = parentFragmentManager.findFragmentByTag(CapsulePreviewDialogFragment.TAG) as DialogFragment? + if (existingDialog == null) { + val dialog = CapsulePreviewDialogFragment.newInstance( + "-1", + event.capsuleId, + event.capsuleType, + false + ) + dialog.show(parentFragmentManager, CapsulePreviewDialogFragment.TAG) + } } + + else -> {} } } } } viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.followLocation.collect { - if (::naverMap.isInitialized) { - if (it) { - naverMap.maxZoom = FIXZOOM - naverMap.minZoom = FIXZOOM - naverMap.locationOverlay.circleRadius = 100 - naverMap.locationOverlay.circleOutlineWidth = 1 - naverMap.locationOverlay.circleColor = + viewModel.followLocation.collect { followLocation -> + clusterer.map?.let { map -> + if (followLocation) { + map.maxZoom = FIXZOOM + map.minZoom = FIXZOOM + map.locationOverlay.circleRadius = 100 + map.locationOverlay.circleOutlineWidth = 1 + map.locationOverlay.circleColor = ContextCompat.getColor(requireContext(), R.color.main_1_alpha20) - naverMap.locationOverlay.circleOutlineColor = + map.locationOverlay.circleOutlineColor = ContextCompat.getColor(requireContext(), R.color.main_1) - naverMap.locationTrackingMode = LocationTrackingMode.Follow + map.locationTrackingMode = LocationTrackingMode.Follow } else { - naverMap.locationTrackingMode = LocationTrackingMode.NoFollow - naverMap.minZoom = MINZOOM - naverMap.maxZoom = MAXZOOM - naverMap.locationOverlay.circleRadius = 0 - + map.locationTrackingMode = LocationTrackingMode.NoFollow + map.minZoom = MINZOOM + map.maxZoom = MAXZOOM + map.locationOverlay.circleRadius = 0 + } + } + } + } + } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.isFriendsCapsuleDisplay.collect { state -> + clusterer.map?.let { map -> + if (state && (viewModel.filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.ALL + || viewModel.filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.PUBLIC) + ) { + viewModel.getNearbyFriendsCapsules( + map.cameraPosition.target.latitude, + map.cameraPosition.target.longitude, + getRadiusForCurrentZoom(), + ) + } else { + viewModel.getNearbyMyCapsules( + map.cameraPosition.target.latitude, + map.cameraPosition.target.longitude, + getRadiusForCurrentZoom(), + viewModel.filterCapsuleSelect.value.name + ) } } - } } } + viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.capsuleList.collect { - removeAllMarkers() - it.map { capsule -> addMarker(capsule) } + clusterer.map?.let { _ -> + // 마커 지우는 로직 + clusterer.clear() + // 마커 찍는 로직 + addMarker(it) + } } } } @@ -179,15 +245,21 @@ class HomeFragment : BaseFragment(R.layo override fun onMapReady(naverMap: NaverMap) { - this.naverMap = naverMap + this.clusterer.map = naverMap + naverMap.uiSettings.isRotateGesturesEnabled = false naverMap.locationSource = locationSource naverMap.locationTrackingMode = LocationTrackingMode.Follow naverMap.minZoom = MINZOOM naverMap.maxZoom = MAXZOOM + + val southWest = LatLng(MINLAT, MINLNG) + val northEast = LatLng(MAXLAT, MAXLNG) + val koreaBounds = LatLngBounds(southWest, northEast) + naverMap.extent = koreaBounds + LocationUtil(requireContext()).getCurrentLocation { latitude, longitude -> val cameraUpdate = CameraUpdate.scrollTo(LatLng(latitude, longitude)) naverMap.moveCamera(cameraUpdate) - } with(naverMap.locationOverlay) { isVisible = true @@ -195,67 +267,76 @@ class HomeFragment : BaseFragment(R.layo } } - private fun addMarker(capsuleMarker: CapsuleMarker) { - val marker = Marker().apply { - position = LatLng(capsuleMarker.latitude, capsuleMarker.longitude) - icon = when (capsuleMarker.capsuleType) { - CapsuleType.SECRET -> OverlayImage.fromResource(R.drawable.ic_marker_pin_secret) - CapsuleType.GROUP -> OverlayImage.fromResource(R.drawable.ic_marker_pin_group) - CapsuleType.PUBLIC -> OverlayImage.fromResource(R.drawable.ic_marker_pin_public) - } - map = naverMap + private fun addMarker(capsuleList: List) { - tag = hashMapOf( - "id" to capsuleMarker.id.toString(), - "type" to CapsuleTypeUtils.enumToString(capsuleMarker.capsuleType) - ) + val keyTagMap: Map = capsuleList.associate { + CapsuleClusteringKey( + id = it.id, + capsuleType = it.capsuleType, + position = LatLng(it.latitude, it.longitude) + ) to null + } - onClickListener = Overlay.OnClickListener { overlay -> - val clickedMarker = overlay as Marker - val markerData = clickedMarker.tag as? HashMap<*, *> - val capsuleId = markerData?.get("id") as? String - val capsuleType = markerData?.get("type") as? String - if (capsuleId != null && capsuleType != null) { - viewModel.homeEvent( - HomeViewModel.HomeEvent.ShowCapsulePreviewDialog( - capsuleId, - capsuleType - ) - ) - } - true + clusterer.addAll(keyTagMap) + } + + private fun fetchCapsulesNearUser() { + locationUtil.getCurrentLocation { latitude, longitude -> + val radius = if (clusterer.map != null) getRadiusForCurrentZoom() else DEFATULTRADIUS + if (viewModel.isFriendsCapsuleDisplay.value && + (viewModel.filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.ALL || + viewModel.filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.PUBLIC) + ) { + viewModel.getNearbyMyAndFriendsCapsules( + latitude, + longitude, + radius, + viewModel.filterCapsuleSelect.value.toString() + ) + } else { + viewModel.getNearbyMyCapsules( + latitude, + longitude, + radius, + viewModel.filterCapsuleSelect.value.toString() + ) } } - markers.add(marker) } - private fun removeAllMarkers() { - markers.forEach { it.map = null } - markers.clear() + private fun fetchCapsulesInCameraFocus() { + clusterer.map?.let { map -> + val cameraTarget = map.cameraPosition.target + if (viewModel.isFriendsCapsuleDisplay.value && + (viewModel.filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.ALL || + viewModel.filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.PUBLIC) + ) { + viewModel.getNearbyMyAndFriendsCapsules( + cameraTarget.latitude, + cameraTarget.longitude, + getRadiusForCurrentZoom(), + viewModel.filterCapsuleSelect.value.toString() + ) + } else { + viewModel.getNearbyMyCapsules( + cameraTarget.latitude, + cameraTarget.longitude, + getRadiusForCurrentZoom(), + viewModel.filterCapsuleSelect.value.toString() + ) + } + } } - // 구현은 했는데 이렇게하면 한국 전체에 생성된 캡슐을 찾기가 어려움 - private fun calculateDistanceInKilometers(): Double { - val projection: Projection = naverMap.projection - - val latLng = projection.fromScreenLocation(Point(0, 0).toPointF()) - val centerLatLng = naverMap.cameraPosition.target + private fun getRadiusForCurrentZoom(): Double { + val currentZoom = clusterer.map?.cameraPosition?.zoom ?: return FIXZOOM - val distanceInMeters = FloatArray(1) - Location.distanceBetween( - centerLatLng.latitude, centerLatLng.longitude, - latLng.latitude, latLng.longitude, - distanceInMeters - ) + val closestZoomLevel = + zoomToRadiusMap.keys.minByOrNull { kotlin.math.abs(it - currentZoom) } - return distanceInMeters[0] / 1000.0 - } - - fun calculateRadiusForZoomLevel(): Double { - val radiusAtMinZoom = 550.0 - val zoomDifference = naverMap.cameraPosition.zoom - MINZOOM - return radiusAtMinZoom * Math.pow(2.0, -zoomDifference) + return zoomToRadiusMap[closestZoomLevel] + ?: throw IllegalArgumentException("Invalid zoom level: $currentZoom") } override fun onRequestPermissionsResult( @@ -269,7 +350,7 @@ class HomeFragment : BaseFragment(R.layo ) ) { if (!locationSource.isActivated) { // 권한 거부됨 - naverMap.locationTrackingMode = LocationTrackingMode.None + clusterer.map?.locationTrackingMode = LocationTrackingMode.None } return } @@ -283,14 +364,33 @@ class HomeFragment : BaseFragment(R.layo PUBLIC } + override fun onResume() { + super.onResume() + fetchCapsulesNearUser() + } + + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (!hidden) { + fetchCapsulesInCameraFocus() + } + } + companion object { const val TAG = "homeFragment" fun newIntent() = HomeFragment() private const val LOCATION_PERMISSION_REQUEST_CODE = 1000 - const val MAXZOOM = 18.0 - const val MINZOOM = 6.0 + const val MAXZOOM = 21.0 + const val MINZOOM = 5.5 const val FIXZOOM = 14.0 + const val MINLAT = 32.0 + const val MAXLAT = 43.0 + const val MINLNG = 124.0 + const val MAXLNG = 132.0 + const val DEFATULTRADIUS = 4.0 + const val MAXRADIUS = 1100.0 + const val MINRADIUS = 2.0 } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModel.kt index 9b6d36bb9..8b43bcaf5 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModel.kt @@ -1,7 +1,6 @@ package com.droidblossom.archive.presentation.ui.home -import com.droidblossom.archive.domain.model.common.CapsuleMarker -import com.droidblossom.archive.presentation.ui.auth.AuthViewModel +import com.droidblossom.archive.domain.model.capsule.CapsuleMarker import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -11,6 +10,7 @@ interface HomeViewModel { val isClickedFAB : StateFlow val existsNotification : StateFlow val followLocation : StateFlow + val isFriendsCapsuleDisplay : StateFlow val capsuleList: StateFlow> val homeEvents: SharedFlow @@ -22,9 +22,13 @@ interface HomeViewModel { fun clickFollowBtn() fun clickNotificationBtn() fun clickFAB() + fun clickFriendsDisplay() fun resetNearbyCapsules() - fun getNearbyCapsules(latitude: Double, longitude: Double, distance: Double, capsuleType: String) + fun getNearbyMyCapsules(latitude: Double, longitude: Double, distance: Double, capsuleType: String) + fun getNearbyFriendsCapsules(latitude: Double, longitude: Double, distance: Double) + fun getNearbyMyAndFriendsCapsules(latitude: Double, longitude: Double, distance: Double, capsuleType: String) + fun homeEvent(event:HomeEvent) @@ -43,6 +47,8 @@ interface HomeViewModel { } sealed class HomeEvent{ + + object GoNotification : HomeEvent() data class ShowCapsulePreviewDialog(val capsuleId: String, val capsuleType: String) : HomeEvent() } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModelImpl.kt index df93325af..465ef5d2f 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/HomeViewModelImpl.kt @@ -2,8 +2,9 @@ package com.droidblossom.archive.presentation.ui.home import android.util.Log import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.domain.model.common.CapsuleMarker -import com.droidblossom.archive.domain.usecase.capsule.NearbyCapsulesUseCase +import com.droidblossom.archive.domain.model.capsule.CapsuleMarker +import com.droidblossom.archive.domain.usecase.capsule.NearbyFriendsCapsulesHomeUseCase +import com.droidblossom.archive.domain.usecase.capsule.NearbyMyCapsulesHomeUseCase import com.droidblossom.archive.presentation.base.BaseViewModel import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess @@ -18,7 +19,8 @@ import javax.inject.Inject @HiltViewModel class HomeViewModelImpl @Inject constructor( - private val nearbyCapsulesUseCase: NearbyCapsulesUseCase, + private val nearbyMyCapsulesHomeUseCase: NearbyMyCapsulesHomeUseCase, + private val nearbyFriendsCapsulesHomeUseCase: NearbyFriendsCapsulesHomeUseCase, ) : BaseViewModel(), HomeViewModel { private val _homeEvents = MutableSharedFlow() @@ -39,7 +41,11 @@ class HomeViewModelImpl @Inject constructor( private val _followLocation: MutableStateFlow = MutableStateFlow(false) override val followLocation: StateFlow get() = _followLocation - private val _capsuleList= MutableStateFlow>(listOf()) + + private val _isFriendsCapsuleDisplay = MutableStateFlow(false) + override val isFriendsCapsuleDisplay: StateFlow + get() = _isFriendsCapsuleDisplay + private val _capsuleList = MutableStateFlow>(listOf()) override val capsuleList: StateFlow> get() = _capsuleList @@ -64,7 +70,8 @@ class HomeViewModelImpl @Inject constructor( if (filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.GROUP) _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.ALL) else - _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.GROUP) } + _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.GROUP) + } } override fun selectSecret() { @@ -73,7 +80,8 @@ class HomeViewModelImpl @Inject constructor( if (filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.SECRET) _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.ALL) else - _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.SECRET) } + _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.SECRET) + } } override fun selectHotPlace() { @@ -81,7 +89,8 @@ class HomeViewModelImpl @Inject constructor( if (filterCapsuleSelect.value == HomeViewModel.CapsuleFilter.HOT) _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.ALL) else - _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.HOT) } + _filterCapsuleSelect.emit(HomeViewModel.CapsuleFilter.HOT) + } } override fun clickFollowBtn() { @@ -92,7 +101,7 @@ class HomeViewModelImpl @Inject constructor( override fun clickNotificationBtn() { viewModelScope.launch { - _existsNotification.emit(!existsNotification.value) + _homeEvents.emit(HomeViewModel.HomeEvent.GoNotification) } } @@ -100,24 +109,85 @@ class HomeViewModelImpl @Inject constructor( viewModelScope.launch { _isClickedFAB.emit(!isClickedFAB.value) } } - override fun resetNearbyCapsules(){ + override fun clickFriendsDisplay() { + viewModelScope.launch { _isFriendsCapsuleDisplay.emit(!isFriendsCapsuleDisplay.value) } + } + + override fun resetNearbyCapsules() { viewModelScope.launch { _capsuleList.emit(emptyList()) } } - override fun getNearbyCapsules(latitude: Double, longitude: Double, distance: Double, capsuleType: String) { - Log.d("티티","$latitude, $longitude, $distance, $capsuleType") + override fun getNearbyMyCapsules( + latitude: Double, + longitude: Double, + distance: Double, + capsuleType: String + ) { + Log.d("티티", "$latitude, $longitude, $distance, $capsuleType") viewModelScope.launch { - nearbyCapsulesUseCase(latitude,longitude, distance, capsuleType ).collect{result-> + nearbyMyCapsulesHomeUseCase( + latitude, + longitude, + distance, + capsuleType + ).collect { result -> result.onSuccess { - _capsuleList.emit(it.capsules) - Log.d("티티","getNearbyCapsules 성공") + _capsuleList.emit(it.capsuleMarkers) }.onFail { - Log.d("티티","getNearbyCapsules 실패") + Log.d("티티", "getNearbyCapsules 실패") } } } } + override fun getNearbyFriendsCapsules(latitude: Double, longitude: Double, distance: Double) { + viewModelScope.launch { + nearbyFriendsCapsulesHomeUseCase( + latitude, + longitude, + distance, + ).collect { result -> + result.onSuccess { + _capsuleList.emit(capsuleList.value + it.capsuleMarkers) + }.onFail { + Log.d("티티", "getNearbyCapsules 실패") + } + } + } + } + + override fun getNearbyMyAndFriendsCapsules( + latitude: Double, + longitude: Double, + distance: Double, + capsuleType: String + ) { + viewModelScope.launch { + nearbyMyCapsulesHomeUseCase( + latitude, + longitude, + distance, + capsuleType + ).collect { result -> + result.onSuccess { + _capsuleList.emit(it.capsuleMarkers) + nearbyFriendsCapsulesHomeUseCase( + latitude, + longitude, + distance, + ).collect { result -> + result.onSuccess { + _capsuleList.emit(capsuleList.value + it.capsuleMarkers) + }.onFail { + Log.d("티티", "getNearbyCapsules 실패") + } + } + }.onFail { + Log.d("티티", "getNearbyCapsules 실패") + } + } + } + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule2Fragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule2Fragment.kt index b1b24b789..e671f5c95 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule2Fragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule2Fragment.kt @@ -10,6 +10,7 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -23,6 +24,8 @@ import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentCreateCapsule2Binding import com.droidblossom.archive.presentation.base.BaseFragment import com.droidblossom.archive.presentation.ui.home.createcapsule.adapter.SkinRVA +import com.droidblossom.archive.presentation.ui.skin.adapter.SkinMotionRVA +import com.droidblossom.archive.util.updateTopConstraintsForSearch import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -36,7 +39,9 @@ class CreateCapsule2Fragment : private lateinit var callback: OnBackPressedCallback private val skinRVA by lazy { - SkinRVA { viewModel.changeSkin(it) } + SkinRVA { previousPosition, currentPosition -> + viewModel.changeSkin(previousPosition, currentPosition) + } } //그룹캡슐이 아닐 경우 바로 엑티비티 닫기 @@ -47,7 +52,8 @@ class CreateCapsule2Fragment : if (viewModel.groupTypeInt != 1) { requireActivity().finish() } else { - super.remove() + isEnabled =false + requireActivity().onBackPressedDispatcher.onBackPressed() } } } @@ -95,6 +101,14 @@ class CreateCapsule2Fragment : viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.isSearchOpen.collect { + val layoutParams = binding.recycleView.layoutParams as ConstraintLayout.LayoutParams + layoutParams.updateTopConstraintsForSearch( + isSearchOpen = it, + searchOpenView = binding.searchOpenBtn, + searchView = binding.searchBtn, + additionalMarginDp = 16f, + resources = resources + ) if (it){ binding.searchOpenEditT.requestFocus() val imm = requireActivity().getSystemService(InputMethodManager::class.java) @@ -108,25 +122,24 @@ class CreateCapsule2Fragment : } - private fun initRVA(){ + + private fun initRVA() { binding.recycleView.adapter = skinRVA - val defaultItemAnimator = DefaultItemAnimator() - defaultItemAnimator.changeDuration = 100 - binding.recycleView.itemAnimator = defaultItemAnimator + binding.recycleView.setHasFixedSize(true) binding.recycleView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) - val lastVisibleItemPosition = - (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition() - val totalItemViewCount = recyclerView.adapter!!.itemCount - 1 - - if (newState == 2 && !recyclerView.canScrollVertically(1) - && lastVisibleItemPosition == totalItemViewCount - ) { - viewModel.getSkinList() + + if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_DRAGGING) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + + if (totalItemCount - lastVisibleItemPosition <= 5) { + viewModel.onScrollNearBottom() + } } } - }) } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule3Fragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule3Fragment.kt index 38575aa28..43853c389 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule3Fragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsule3Fragment.kt @@ -120,6 +120,15 @@ class CreateCapsule3Fragment : nextBtn.setOnClickListener { + if (viewModel.isSelectTimeCapsule.value && (viewModel.capsuleLatitude.value == 0.0 || viewModel.capsuleTitle.value.isEmpty() || viewModel.capsuleContent.value.isEmpty() || viewModel.dueTime.value.isEmpty())) { + showToastMessage("타임캡슐은 시간, 제목, 내용이 필수 입니다.") + return@setOnClickListener + } + if (!viewModel.isSelectTimeCapsule.value && (viewModel.capsuleLatitude.value == 0.0 || viewModel.capsuleTitle.value.isEmpty() || viewModel.capsuleContent.value.isEmpty())) { + showToastMessage("캡슐은 제목, 내용이 필수 입니다.") + return@setOnClickListener + } + showLoading(requireContext()) val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) val currentTime = dateFormat.format(Date()) CoroutineScope(Dispatchers.IO).launch { @@ -209,7 +218,14 @@ class CreateCapsule3Fragment : showToastMessage(it.message) } + CreateCapsuleViewModel.Create3Event.ClickVideoUpLoad -> {} + + CreateCapsuleViewModel.Create3Event.DismissLoading -> { + dismissLoading() + } + + else -> {} } } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleActivity.kt index 35b368f2a..83608cf76 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleActivity.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleActivity.kt @@ -6,9 +6,6 @@ import android.os.Bundle import android.util.Log import android.view.ViewGroup import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContentProviderCompat.requireContext -import androidx.navigation.findNavController import com.droidblossom.archive.R import com.droidblossom.archive.databinding.ActivityCreateCapsuleBinding import com.droidblossom.archive.presentation.base.BaseActivity diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModel.kt index d0951995c..3910d5857 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModel.kt @@ -59,13 +59,14 @@ interface CreateCapsuleViewModel { val dueTime : StateFlow val address : StateFlow + fun onScrollNearBottom() fun move1To2() fun choseCapsuleType(type: Int) fun move2To3() fun openSearchSkin() fun closeSearchSkin() fun searchSkin() - fun changeSkin(skin: CapsuleSkinSummary) + fun changeSkin(previousPosition: Int?, currentPosition: Int) fun getSkinList() fun moveFinish() fun moveLocation() @@ -85,7 +86,6 @@ interface CreateCapsuleViewModel { fun coordToAddress(latitude: Double, longitude: Double) fun getDueTime(tiem:String) fun getUploadUrls(getS3UrlData : S3UrlRequest) - fun setFiles(imageFiles: List, videoFiles: List) fun closeTimeSetting() fun openTimeSetting() @@ -108,6 +108,8 @@ interface CreateCapsuleViewModel { object ClickImgUpLoad : Create3Event() object CLickSingleImgUpLoad : Create3Event() object ClickVideoUpLoad : Create3Event() + + object DismissLoading : Create3Event() data class ShowToastMessage(val message : String) : Create3Event() } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModelImpl.kt index a2562e838..1d98e1e6e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/CreateCapsuleViewModelImpl.kt @@ -3,27 +3,22 @@ package com.droidblossom.archive.presentation.ui.home.createcapsule import android.net.Uri import android.util.Log import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.domain.model.common.AddressData import com.droidblossom.archive.domain.model.common.CapsuleSkinSummary import com.droidblossom.archive.domain.model.common.Dummy import com.droidblossom.archive.domain.model.common.FileName import com.droidblossom.archive.domain.model.common.Location -import com.droidblossom.archive.domain.model.common.Skin import com.droidblossom.archive.domain.model.s3.S3UrlRequest -import com.droidblossom.archive.domain.model.secret.SecretCapsuleCreateRequest +import com.droidblossom.archive.domain.model.common.CapsuleCreateRequest import com.droidblossom.archive.domain.usecase.capsule.GetAddressUseCase import com.droidblossom.archive.domain.usecase.capsule_skin.CapsuleSkinsPageUseCase -import com.droidblossom.archive.domain.usecase.kakao.ToAddressUseCase +import com.droidblossom.archive.domain.usecase.open.PublicCapsuleCreateUseCase import com.droidblossom.archive.domain.usecase.s3.S3UrlsGetUseCase import com.droidblossom.archive.domain.usecase.secret.SecretCapsuleCreateUseCase import com.droidblossom.archive.presentation.base.BaseViewModel -import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleViewModelImpl.Companion.S3DIRECTORY import com.droidblossom.archive.util.DateUtils -import com.droidblossom.archive.util.FileUtils import com.droidblossom.archive.util.S3Util -import com.droidblossom.archive.util.onError -import com.droidblossom.archive.util.onException import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel @@ -32,20 +27,23 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.joinAll +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import java.io.File +import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel class CreateCapsuleViewModelImpl @Inject constructor( private val getAddressUseCase: GetAddressUseCase, private val secretCapsuleCreateUseCase: SecretCapsuleCreateUseCase, + private val publicCapsuleCreateUseCase: PublicCapsuleCreateUseCase, private val s3UrlsGetUseCase: S3UrlsGetUseCase, private val capsuleSkinsPageUseCase: CapsuleSkinsPageUseCase, private val s3Util: S3Util, @@ -178,6 +176,25 @@ class CreateCapsuleViewModelImpl @Inject constructor( private var imageNames = listOf() private var videoNames = listOf() + private val scrollEventChannel = Channel(Channel.CONFLATED) + private val scrollEventFlow = + scrollEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) + + private var getLstJob: Job? = null + + init { + viewModelScope.launch { + scrollEventFlow.collect { + getSkinList() + } + } + } + + override fun onScrollNearBottom() { + scrollEventChannel.trySend(Unit) + } + + //create1 override fun move1To2() { viewModelScope.launch { @@ -236,28 +253,35 @@ class CreateCapsuleViewModelImpl @Inject constructor( } } - override fun changeSkin(skin: CapsuleSkinSummary) { - val submitList = skins.value - submitList.map { it.isClicked = false } - submitList[submitList.indexOf(skin)].isClicked = true + override fun changeSkin(previousPosition: Int?, currentPosition: Int) { viewModelScope.launch { - _skins.emit(submitList) + val newList = _skins.value + previousPosition?.let { + newList[it].isClicked = false + } + newList[currentPosition].isClicked = true + _skins.emit(newList) } } override fun getSkinList() { - viewModelScope.launch { - if (hasNextSkins.value) { + if (hasNextSkins.value) { + getLstJob?.cancel() + getLstJob = viewModelScope.launch { capsuleSkinsPageUseCase( - CapsuleSkinsPageRequestDto( + PagingRequestDto( 15, _lastCreatedSkinTime.value ) ).collect { result -> result.onSuccess { - _skins.emit(it.skins) _hasNextSkins.emit(it.hasNext) - _lastCreatedSkinTime.emit(it.skins.last().createdAt) + if (skins.value.isEmpty()) { + _skins.emit(it.skins) + } else { + _skins.emit(skins.value + it.skins) + } + _lastCreatedSkinTime.value = _skins.value.last().createdAt }.onFail { _create2Events.emit(CreateCapsuleViewModel.Create2Event.ShowToastMessage("스킨 불러오기 실패.")) } @@ -457,6 +481,7 @@ class CreateCapsuleViewModelImpl @Inject constructor( it.preSignedVideoUrls ) }.onFail { + _create3Events.emit(CreateCapsuleViewModel.Create3Event.DismissLoading) Log.d("getUploadUrls", "getUploadUrl 실패") } } @@ -519,6 +544,7 @@ class CreateCapsuleViewModelImpl @Inject constructor( Log.d("uploadFilesToS3", "All files uploaded successfully") createCapsule() } else { + _create3Events.emit(CreateCapsuleViewModel.Create3Event.DismissLoading) Log.e("uploadFilesToS3", "One or more file uploads failed") } @@ -529,12 +555,40 @@ class CreateCapsuleViewModelImpl @Inject constructor( viewModelScope.launch { when (capsuleTypeCreateIs.value) { CreateCapsuleViewModel.CapsuleTypeCreate.PUBLIC -> { - + publicCapsuleCreateUseCase( + CapsuleCreateRequest( + capsuleSkinId = skinId.value, + content = capsuleContent.value, + directory = S3DIRECTORY, + dueDate = dueTime.value, + imageNames = imageNames, + videoNames = videoNames, + addressData = address.value, + latitude = capsuleLatitude.value, + longitude = capsuleLongitude.value, + title = capsuleTitle.value, + ) + ).collect { result -> + result.onSuccess { + _create3Events.emit( + CreateCapsuleViewModel.Create3Event.ShowToastMessage( + "캡슐이 생성되었습니다." + ) + ) + Log.d("캡슐생성", "$it") + }.onFail { + _create3Events.emit( + CreateCapsuleViewModel.Create3Event.ShowToastMessage( + "캡슐이 생성 실패했습니다.." + ) + ) + } + } } CreateCapsuleViewModel.CapsuleTypeCreate.SECRET -> { secretCapsuleCreateUseCase( - SecretCapsuleCreateRequest( + CapsuleCreateRequest( capsuleSkinId = skinId.value, content = capsuleContent.value, directory = S3DIRECTORY, @@ -548,10 +602,18 @@ class CreateCapsuleViewModelImpl @Inject constructor( ) ).collect { result -> result.onSuccess { - _create3Events.emit(CreateCapsuleViewModel.Create3Event.ShowToastMessage("캡슐이 생성되었습니다.")) + _create3Events.emit( + CreateCapsuleViewModel.Create3Event.ShowToastMessage( + "캡슐이 생성되었습니다." + ) + ) Log.d("캡슐생성", "$it") }.onFail { - _create3Events.emit(CreateCapsuleViewModel.Create3Event.ShowToastMessage("캡슐이 생성 실패했습니다..")) + _create3Events.emit( + CreateCapsuleViewModel.Create3Event.ShowToastMessage( + "캡슐이 생성 실패했습니다.." + ) + ) } } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/adapter/SkinRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/adapter/SkinRVA.kt index 92a4d8645..ca0cdf6e4 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/adapter/SkinRVA.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/adapter/SkinRVA.kt @@ -1,34 +1,34 @@ package com.droidblossom.archive.presentation.ui.home.createcapsule.adapter -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup -import androidx.datastore.preferences.core.preferencesOf import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.droidblossom.archive.databinding.ItemSkinBinding import com.droidblossom.archive.domain.model.common.CapsuleSkinSummary -import com.droidblossom.archive.domain.model.common.Skin -class SkinRVA( val SkinFlow: (CapsuleSkinSummary) -> Unit) : +class SkinRVA(val SkinFlow: (previousPosition: Int?, currentPosition: Int) -> Unit) : ListAdapter(differ) { + private var previousClickedPosition: Int? = null + inner class ItemViewHolder( private val binding: ItemSkinBinding ) : RecyclerView.ViewHolder(binding.root) { - //@SuppressLint("NotifyDataSetChanged") fun bind(data: CapsuleSkinSummary) { binding.item = data binding.root.setOnClickListener { - notifyItemChanged(currentList.indexOf( - currentList.find { - it.isClicked + val currentClickedPosition = bindingAdapterPosition + if (currentClickedPosition != RecyclerView.NO_POSITION) { + SkinFlow(previousClickedPosition, currentClickedPosition) + previousClickedPosition?.let { previousClickedPosition -> + notifyItemChanged(previousClickedPosition) } - )) - SkinFlow(data) - notifyItemChanged(currentList.indexOf(data)) + notifyItemChanged(currentClickedPosition) + previousClickedPosition = currentClickedPosition + } } } } @@ -52,11 +52,17 @@ class SkinRVA( val SkinFlow: (CapsuleSkinSummary) -> Unit) : companion object { val differ = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: CapsuleSkinSummary, newItem: CapsuleSkinSummary): Boolean { + override fun areItemsTheSame( + oldItem: CapsuleSkinSummary, + newItem: CapsuleSkinSummary + ): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: CapsuleSkinSummary, newItem: CapsuleSkinSummary): Boolean { + override fun areContentsTheSame( + oldItem: CapsuleSkinSummary, + newItem: CapsuleSkinSummary + ): Boolean { return oldItem == newItem } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/dialog/DatePickerDialogFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/dialog/DatePickerDialogFragment.kt index 5e671e5c4..c0e0d0c3e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/dialog/DatePickerDialogFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/createcapsule/dialog/DatePickerDialogFragment.kt @@ -3,6 +3,7 @@ package com.droidblossom.archive.presentation.ui.home.createcapsule.dialog import android.os.Bundle import android.util.Log import android.view.View +import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -31,6 +32,16 @@ class DatePickerDialogFragment(private val onClick: (String, String) -> Unit) : private val currentHour = DateUtils.getCurrentHour() private val currentMin = DateUtils.getCurrentMin() + override fun onStart() { + super.onStart() + val dialog = dialog + if (dialog != null) { + val width = ViewGroup.LayoutParams.MATCH_PARENT + val height = ViewGroup.LayoutParams.WRAP_CONTENT + dialog.window?.setLayout(width, height) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.vm = viewModel diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogFragment.kt index 0c1ac31a9..088bca0a0 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogFragment.kt @@ -6,10 +6,18 @@ import android.animation.ObjectAnimator import android.content.DialogInterface import android.os.Bundle import android.view.Gravity +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.LinearInterpolator +import android.widget.Button +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.PopupWindow +import android.widget.TextView +import android.widget.Toast import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.ContextCompat import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -17,6 +25,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentCapsulePreviewDialogBinding +import com.droidblossom.archive.databinding.PopupMenuCapsuleBinding import com.droidblossom.archive.presentation.base.BaseDialogFragment import com.droidblossom.archive.presentation.ui.capsule.CapsuleDetailActivity import com.droidblossom.archive.presentation.ui.home.HomeFragment @@ -31,8 +40,13 @@ class CapsulePreviewDialogFragment : private val viewModel: CapsulePreviewDialogViewModelImpl by viewModels() - val capsuleId: Int by lazy { - arguments?.getString("capsule_id")!!.toInt() + val capsuleIndex: Int by lazy { + arguments?.getString("capsule_index")!!.toInt() + } + + + val capsuleId: Long by lazy { + arguments?.getString("capsule_id")!!.toLong() } val capsuleType by lazy { @@ -72,7 +86,8 @@ class CapsulePreviewDialogFragment : } HomeFragment.CapsuleType.PUBLIC -> { - + viewModel.getPublicCapsuleSummary(capsuleId) + viewModel.setCapsuleTypeImage(R.drawable.ic_public_marker_24) } HomeFragment.CapsuleType.GROUP -> { @@ -100,8 +115,13 @@ class CapsulePreviewDialogFragment : viewModel.openCapsule(capsuleId.toLong()) } } + + capsuleMenuImg.setOnClickListener { view -> + showPopupMenu(view) + } } } + private fun initObserver() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -119,7 +139,7 @@ class CapsulePreviewDialogFragment : } } - viewLifecycleOwner.lifecycleScope.launch{ + viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.capsulePreviewDialogEvents.collect { event -> when (event) { @@ -178,14 +198,19 @@ class CapsulePreviewDialogFragment : constraintSet.applyTo(constraintLayout) } - private fun moveCapsuleDetail(){ - val intent = CapsuleDetailActivity.newIntent(requireContext(), capsuleId.toLong(), capsuleType!!) + private fun moveCapsuleDetail() { + val intent = + CapsuleDetailActivity.newIntent(requireContext(), capsuleId.toLong(), capsuleType!!) startActivity(intent) } private fun animateProgressBar() { - viewModel.capsulePreviewDialogEvent(CapsulePreviewDialogViewModel.CapsulePreviewDialogEvent.ShowToastMessage("캡슐이 열리는 중입니다.")) + viewModel.capsulePreviewDialogEvent( + CapsulePreviewDialogViewModel.CapsulePreviewDialogEvent.ShowToastMessage( + "캡슐이 열리는 중입니다." + ) + ) val animator = ObjectAnimator.ofInt(binding.openProgressBar, "progress", 0, 100).apply { duration = 2000 // 2초 동안 interpolator = LinearInterpolator() // 여기에 LinearInterpolator 적용 @@ -200,9 +225,41 @@ class CapsulePreviewDialogFragment : animator.start() } + private fun showPopupMenu(view: View) { + val popupMenuBinding = + PopupMenuCapsuleBinding.inflate(LayoutInflater.from(requireContext()), null, false) + + val density = requireContext().resources.displayMetrics.density + val widthPixels = (120 * density).toInt() + + val popupWindow = PopupWindow( + popupMenuBinding.root, + widthPixels, + LinearLayout.LayoutParams.WRAP_CONTENT, + true + ) + + popupWindow.contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val popupWidth = popupWindow.contentView.measuredWidth + + popupMenuBinding.menuMap.setOnClickListener { + popupWindow.dismiss() + } + popupMenuBinding.menuModify.setOnClickListener { + popupWindow.dismiss() + } + popupMenuBinding.menuDelete.setOnClickListener { + popupWindow.dismiss() + } + + val xOffset = (view.width / 2) - (popupWidth / 2) - 200 + popupWindow.showAsDropDown(view, xOffset, -view.height) + } + override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) val capsuleState = Bundle().apply { + putInt("capsuleIndex", capsuleIndex) putLong("capsuleId", capsuleId.toLong()) putBoolean("isOpened", viewModel.capsuleOpenState.value) } @@ -210,8 +267,16 @@ class CapsulePreviewDialogFragment : } companion object { - fun newInstance(capsuleId: String, capsuleType: String, calledFromCamera : Boolean): CapsulePreviewDialogFragment { + + const val TAG = "CapsulePreView" + fun newInstance( + capsuleIndex: String, + capsuleId: String, + capsuleType: String, + calledFromCamera: Boolean + ): CapsulePreviewDialogFragment { val args = Bundle().apply { + putString("capsule_index", capsuleIndex) putString("capsule_id", capsuleId) putString("capsule_type", capsuleType) putBoolean("called_from_camera", calledFromCamera) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModel.kt index e4de8b46d..58dbc0f5e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModel.kt @@ -1,6 +1,6 @@ package com.droidblossom.archive.presentation.ui.home.dialog -import com.droidblossom.archive.domain.model.secret.SecretCapsuleSummary +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import java.util.Calendar @@ -8,7 +8,7 @@ import java.util.Calendar interface CapsulePreviewDialogViewModel { val capsulePreviewDialogEvents: SharedFlow - val secretCapsuleSummary : StateFlow + val capsuleSummaryResponse : StateFlow val startTime: StateFlow val endTime: StateFlow val totalTime: StateFlow @@ -33,6 +33,8 @@ interface CapsulePreviewDialogViewModel { fun setCalledFromCamera(calledFromCamera : Boolean) + fun getSecretCapsuleSummary(capsuleId: Long) + fun getPublicCapsuleSummary(capsuleId: Long) sealed class CapsulePreviewDialogEvent{ data class ShowToastMessage(val message : String) : CapsulePreviewDialogEvent() diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModelImpl.kt index f1658b5c9..a60bf06c0 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/dialog/CapsulePreviewDialogViewModelImpl.kt @@ -2,8 +2,9 @@ package com.droidblossom.archive.presentation.ui.home.dialog import android.util.Log import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.domain.model.secret.SecretCapsuleSummary +import com.droidblossom.archive.domain.model.common.CapsuleSummaryResponse import com.droidblossom.archive.domain.usecase.capsule.PatchCapsuleOpenedUseCase +import com.droidblossom.archive.domain.usecase.open.PublicCapsuleSummaryUseCase import com.droidblossom.archive.domain.usecase.secret.SecretCapsuleSummaryUseCase import com.droidblossom.archive.presentation.base.BaseViewModel import com.droidblossom.archive.util.onError @@ -28,15 +29,16 @@ import javax.inject.Inject @HiltViewModel class CapsulePreviewDialogViewModelImpl @Inject constructor( private val secretCapsuleSummaryUseCase: SecretCapsuleSummaryUseCase, + private val publicCapsuleSummaryUseCase: PublicCapsuleSummaryUseCase, private val patchCapsuleOpenedUseCase: PatchCapsuleOpenedUseCase ) : BaseViewModel(), CapsulePreviewDialogViewModel { private val _capsulePreviewDialogEvents = MutableSharedFlow() override val capsulePreviewDialogEvents: SharedFlow = _capsulePreviewDialogEvents.asSharedFlow() - private val _secretCapsuleSummary = MutableStateFlow(SecretCapsuleSummary("","","","","","","", false, "")) - override val secretCapsuleSummary: StateFlow - get() = _secretCapsuleSummary + private val _capsuleSummaryResponse = MutableStateFlow(CapsuleSummaryResponse("","","","","","","", false, "")) + override val capsuleSummaryResponse: StateFlow + get() = _capsuleSummaryResponse private val _startTime = MutableStateFlow(null) private val _endTime = MutableStateFlow(null) @@ -84,20 +86,32 @@ class CapsulePreviewDialogViewModelImpl @Inject constructor( override fun setCalledFromCamera(calledFromCamera : Boolean){ _calledFromCamera.value = calledFromCamera } - fun getSecretCapsuleSummary(capsuleId: Int) { + override fun getSecretCapsuleSummary(capsuleId: Long) { viewModelScope.launch { secretCapsuleSummaryUseCase(capsuleId).collect { result -> result.onSuccess { - _secretCapsuleSummary.emit(it) - _capsuleOpenState.emit(secretCapsuleSummary.value.isOpened) + _capsuleSummaryResponse.emit(it) + _capsuleOpenState.emit(capsuleSummaryResponse.value.isOpened) if (!capsuleOpenState.value){ calculateCapsuleOpenTime(it.createdAt, it.dueDate) } }.onFail { - }.onException { + } + } + } + } - }.onError { + override fun getPublicCapsuleSummary(capsuleId: Long) { + viewModelScope.launch { + publicCapsuleSummaryUseCase(capsuleId).collect { result -> + result.onSuccess { + _capsuleSummaryResponse.emit(it) + _capsuleOpenState.emit(capsuleSummaryResponse.value.isOpened) + if (!capsuleOpenState.value){ + calculateCapsuleOpenTime(it.createdAt, it.dueDate) + } + }.onFail { } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationActivity.kt new file mode 100644 index 000000000..66635f239 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationActivity.kt @@ -0,0 +1,136 @@ +package com.droidblossom.archive.presentation.ui.home.notification + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivityNotificationBinding +import com.droidblossom.archive.domain.model.member.NotiCategoryName +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.ui.home.notification.adapter.NotificationRVA +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendActivity +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendActivity.Companion.FRIEND +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.FriendAcceptActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class NotificationActivity : + BaseActivity(R.layout.activity_notification) { + + override val viewModel: NotificationViewModelImpl by viewModels() + + private val notificationRVA by lazy { + NotificationRVA { + when (it.categoryName) { + NotiCategoryName.CAPSULE_SKIN -> { + + } + + NotiCategoryName.FRIEND_REQUEST -> { + startActivity(FriendAcceptActivity.newIntent(this, FriendAcceptActivity.FRIEND)) + } + + NotiCategoryName.GROUP_REQUEST -> { + startActivity(FriendAcceptActivity.newIntent(this, FriendAcceptActivity.GROUP)) + } + + NotiCategoryName.FRIEND_ACCEPT -> { + startActivity(FriendActivity.newIntent(this, FriendActivity.FRIEND)) + } + + NotiCategoryName.GROUP_ACCEPT -> { + startActivity(FriendActivity.newIntent(this, FriendActivity.GROUP)) + } + + else -> {} + } + } + } + + private val requestNotificationLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + + } else { + showToastMessage("ARchive 앱의 알림을 받기 위해서는 알림 권한이 필요합니다. 알림을 통해 중요한 정보와 업데이트를 놓치지 마세요.") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding.vm = viewModel + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestNotificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + initView() + viewModel.getNotificationPage() + } + + private fun initView() { + val layoutParams = binding.backBtn.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.backBtn.layoutParams = layoutParams + + binding.backBtn.setOnClickListener { + finish() + } + binding.rv.adapter = notificationRVA + binding.rv.setHasFixedSize(true) + binding.rv.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + val lastVisibleItemPosition = + (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition() + val totalItemViewCount = recyclerView.adapter!!.itemCount - 1 + + if (newState == 2 && !recyclerView.canScrollVertically(1) + && lastVisibleItemPosition == totalItemViewCount + ) { + viewModel.onScrollNearBottom() + } + } + }) + } + + override fun observeData() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.notifications.collect { notifications -> + notificationRVA.submitList(notifications) + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.notificationEvent.collect { event -> + when (event) { + is NotificationViewModel.NotificationEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + } + + companion object { + const val NOTIFICATION = "notificaiton" + + fun newIntent(context: Context) = + Intent(context, NotificationActivity::class.java) + } +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationViewModel.kt new file mode 100644 index 000000000..1e6c8387a --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationViewModel.kt @@ -0,0 +1,20 @@ +package com.droidblossom.archive.presentation.ui.home.notification + +import com.droidblossom.archive.domain.model.member.NotificationModel +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface NotificationViewModel { + val notificationEvent: SharedFlow + + val notifications: StateFlow> + val hasNextPage: StateFlow + val lastCreatedTime: StateFlow + + fun getNotificationPage() + fun onScrollNearBottom() + + sealed class NotificationEvent { + data class ShowToastMessage(val message: String) : NotificationEvent() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationViewModelImpl.kt new file mode 100644 index 000000000..5dbeb54ba --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/NotificationViewModelImpl.kt @@ -0,0 +1,91 @@ +package com.droidblossom.archive.presentation.ui.home.notification + +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.member.NotificationModel +import com.droidblossom.archive.domain.usecase.member.GetNotificationsUseCase +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.presentation.base.BaseViewModel.Companion.throttleFirst +import com.droidblossom.archive.util.DateUtils +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class NotificationViewModelImpl @Inject constructor( + private val getNotificationsUseCase: GetNotificationsUseCase +) : BaseViewModel(), NotificationViewModel { + + private val _notificationEvent = MutableSharedFlow() + override val notificationEvent: SharedFlow + get() = _notificationEvent.asSharedFlow() + + private val _notifications = MutableStateFlow(listOf()) + + override val notifications: StateFlow> + get() = _notifications + + private val _hasNextPage = MutableStateFlow(true) + override val hasNextPage: StateFlow + get() = _hasNextPage + + private val _lastCreatedTime = MutableStateFlow(DateUtils.dataServerString) + override val lastCreatedTime: StateFlow + get() = _lastCreatedTime + + private val scrollEventChannel = Channel(Channel.CONFLATED) + private val scrollEventFlow = + scrollEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) + + private var getNotificationListJob: Job? = null + + init { + viewModelScope.launch { + scrollEventFlow.collect { + getNotificationPage() + } + } + } + + override fun onScrollNearBottom() { + scrollEventChannel.trySend(Unit) + } + + override fun getNotificationPage() { + if (hasNextPage.value) { + getNotificationListJob?.cancel() + getNotificationListJob = viewModelScope.launch { + getNotificationsUseCase( + PagingRequestDto( + 15, + lastCreatedTime.value + ) + ).collect { result -> + result.onSuccess { + _hasNextPage.value = it.hasNext + _notifications.emit(notifications.value + it.notifications) + _lastCreatedTime.value = it.notifications.last().createdAt + }.onFail { + _notificationEvent.emit( + NotificationViewModel.NotificationEvent.ShowToastMessage( + "알림 불러오기 실패" + ) + ) + } + } + } + } + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/adapter/NotificationRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/adapter/NotificationRVA.kt new file mode 100644 index 000000000..6b843203d --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/home/notification/adapter/NotificationRVA.kt @@ -0,0 +1,52 @@ +package com.droidblossom.archive.presentation.ui.home.notification.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemNotificationBinding +import com.droidblossom.archive.domain.model.member.NotificationModel + +class NotificationRVA(val onClick: (NotificationModel) -> Unit) : + ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemNotificationBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: NotificationModel) { + binding.item = data + binding.root.setOnClickListener { + onClick(data) + } + } + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: NotificationModel, newItem: NotificationModel + ): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: NotificationModel, newItem: NotificationModel + ): Boolean { + return oldItem == newItem + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder( + ItemNotificationBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageFragment.kt index 78a6d665c..c2a9ce5c4 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageFragment.kt @@ -1,42 +1,75 @@ package com.droidblossom.archive.presentation.ui.mypage +import android.Manifest +import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import android.view.View import android.view.ViewGroup -import android.widget.AbsListView -import android.widget.AbsListView.OnScrollListener -import androidx.core.content.ContentProviderCompat.requireContext -import androidx.core.content.ContextCompat.startActivity +import androidx.core.app.ActivityCompat +import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentMyPageBinding -import com.droidblossom.archive.domain.model.common.MyCapsule import com.droidblossom.archive.presentation.base.BaseFragment import com.droidblossom.archive.presentation.ui.capsule.CapsuleDetailActivity -import com.droidblossom.archive.presentation.ui.home.HomeFragment -import com.droidblossom.archive.presentation.ui.home.createcapsule.adapter.SkinRVA import com.droidblossom.archive.presentation.ui.home.dialog.CapsulePreviewDialogFragment -import com.droidblossom.archive.presentation.ui.mypage.adapter.MyCapsuleRVA -import com.droidblossom.archive.presentation.ui.skin.SkinFragment -import com.droidblossom.archive.util.DateUtils +import com.droidblossom.archive.presentation.ui.mypage.adapter.CapsuleRVA +import com.droidblossom.archive.presentation.ui.mypage.adapter.ProfileRVA +import com.droidblossom.archive.presentation.ui.mypage.adapter.SpinnerAdapter +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendActivity +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.FriendAcceptActivity +import com.droidblossom.archive.presentation.ui.mypage.setting.SettingActivity +import com.droidblossom.archive.util.CustomLifecycleOwner import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch + @AndroidEntryPoint -class MyPageFragment : - BaseFragment(R.layout.fragment_my_page) { +class MyPageFragment : BaseFragment(R.layout.fragment_my_page) { + override val viewModel: MyPageViewModelImpl by viewModels() + private var reloadMyInfo = false + + private val visibleLifecycleOwner: CustomLifecycleOwner by lazy { + CustomLifecycleOwner() + } - private val myCapsuleRVA by lazy { - MyCapsuleRVA( + private val profileRVA: ProfileRVA by lazy { + ProfileRVA( + { + startActivity(FriendActivity.newIntent(requireContext(), FriendActivity.GROUP)) + reloadMyInfo = true + }, + { + startActivity(FriendActivity.newIntent(requireContext(), FriendActivity.FRIEND)) + reloadMyInfo = true + }, + { + startActivity( + FriendAcceptActivity.newIntent( + requireContext(), + FriendAcceptActivity.FRIEND + ) + ) + reloadMyInfo = true + }, + { + startActivity(SettingActivity.newIntent(requireContext())) + } + ) + } + private val capsuleRVA: CapsuleRVA by lazy { + CapsuleRVA( { id, type -> startActivity( CapsuleDetailActivity.newIntent( @@ -46,99 +79,263 @@ class MyPageFragment : ) ) }, - { id, type -> - val sheet = CapsulePreviewDialogFragment.newInstance(id.toString(), type.toString(), false) - sheet.show(parentFragmentManager, "CapsulePreviewDialog") - }, + { capsuleIndex, id, type -> + val existingDialog = + parentFragmentManager.findFragmentByTag(CapsulePreviewDialogFragment.TAG) as DialogFragment? + if (existingDialog == null) { + val dialog = CapsulePreviewDialogFragment.newInstance( + capsuleIndex.toString(), + id.toString(), + type.toString(), + false + ) + dialog.show(parentFragmentManager, CapsulePreviewDialogFragment.TAG) + } + + } ) } + private val capsuleTypes = arrayOf( + SpinnerCapsuleType.SECRET, SpinnerCapsuleType.PUBLIC, SpinnerCapsuleType.GROUP + ) + + private val spinnerA: SpinnerAdapter by lazy { + SpinnerAdapter( + requireContext(), + capsuleTypes + ) { capsuleTypes -> + viewModel.selectSpinnerItem(capsuleTypes) + + } + } + + private val concatAdapter: ConcatAdapter by lazy { + val config = ConcatAdapter.Config.Builder() + .setIsolateViewTypes(true) + .setStableIdMode(ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS) + .build() + ConcatAdapter(config, profileRVA, spinnerA, capsuleRVA) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.vm = viewModel - viewModel.getMe() - viewModel.getSecretCapsulePage() - parentFragmentManager.setFragmentResultListener("capsuleState", viewLifecycleOwner) { key, bundle -> + parentFragmentManager.setFragmentResultListener( + "capsuleState", + viewLifecycleOwner + ) { key, bundle -> + val capsuleIndex = bundle.getInt("capsuleIndex") val capsuleId = bundle.getLong("capsuleId") val capsuleOpenState = bundle.getBoolean("isOpened") - viewModel.updateCapsuleOpenState(capsuleId,capsuleOpenState) - } - - initRVA() - - binding.settingBtn.setOnClickListener { - throw RuntimeException("Test Crash") + if (capsuleIndex != -1 && capsuleOpenState) { + viewModel.updateCapsuleOpenState(capsuleIndex, capsuleId) + capsuleRVA.notifyItemChanged(capsuleIndex) + } } - - val layoutParams = binding.profileImg.layoutParams as ViewGroup.MarginLayoutParams + initCustomLifeCycle() + initMyPageRVA() + val layoutParams = + binding.myPageSwipeRefreshLayout.layoutParams as ViewGroup.MarginLayoutParams layoutParams.topMargin += getStatusBarHeight() - binding.profileImg.layoutParams = layoutParams + binding.myPageSwipeRefreshLayout.layoutParams = layoutParams } - private fun initRVA() { - binding.capsuleRecycleView.adapter = myCapsuleRVA - binding.capsuleRecycleView.animation = null - //무한 스크롤 - binding.capsuleRecycleView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + private fun initMyPageRVA() { + val layoutManager = GridLayoutManager(requireContext(), 3) + layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + var totalItemsCounted = 0 + concatAdapter.adapters.forEach { adapter -> + val itemCount = adapter.itemCount + if (position < totalItemsCounted + itemCount) { + val localPosition = position - totalItemsCounted + val viewType = adapter.getItemViewType(localPosition) + return when (viewType) { + PROFILE_TYPE -> 3 + CAPSULE_TYPE -> 1 + else -> 3 + } + } + totalItemsCounted += itemCount + } + return 3 + } + } + binding.myPageRV.layoutManager = layoutManager + binding.myPageRV.adapter = concatAdapter + binding.myPageSwipeRefreshLayout.setOnRefreshListener { + viewModel.load() + } + + binding.myPageRV.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) - val lastVisibleItemPosition = - (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition() - val totalItemViewCount = recyclerView.adapter!!.itemCount - 1 - - if (newState == 2 && !recyclerView.canScrollVertically(1) - && lastVisibleItemPosition == totalItemViewCount - ) { - viewModel.getSecretCapsulePage() + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + val totalItemCount = recyclerView.adapter!!.itemCount + val threshold = 6 + + if (lastVisibleItemPosition >= totalItemCount - threshold) { + viewModel.onScrollNearBottom() } } - }) + } override fun observeData() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.myCapsules.collect { capsule -> - if (capsule.isNotEmpty()){ + if (viewModel.clearCapsule) { + viewModel.clearCapsule = false + } else { viewModel.updateMyCapsulesUI() } } } } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.myCapsulesUI.collect { capsule -> - myCapsuleRVA.submitList(capsule) + capsuleRVA.submitList(capsule) { + if (binding.myPageSwipeRefreshLayout.isRefreshing) { + binding.myPageSwipeRefreshLayout.isRefreshing = false + binding.myPageRV.scrollToPosition(0) + } + } } } } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { + + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.myPageEvents.collect { event -> when (event) { is MyPageViewModel.MyPageEvent.ShowToastMessage -> { showToastMessage(event.message) } + is MyPageViewModel.MyPageEvent.ClickSetting -> { + startActivity(SettingActivity.newIntent(requireContext())) + } + + is MyPageViewModel.MyPageEvent.HideLoading -> { + binding.myPageSwipeRefreshLayout.isRefreshing = false + } + else -> {} } } } } + + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.myInfo.collect { memberDetail -> + profileRVA.submitList(listOf(memberDetail.toUIModel())) + } + } + } + + visibleLifecycleOwner.lifecycleScope.launch { + visibleLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.capsuleType.collect { + if (viewModel.viewModelReload){ + viewModel.clearCapsules(false) + } + + } + } + } + } + + private fun initCustomLifeCycle() { + viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_START, + Lifecycle.Event.ON_CREATE, + Lifecycle.Event.ON_RESUME, + Lifecycle.Event.ON_PAUSE, -> { + if (isHidden) { + visibleLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } else { + visibleLifecycleOwner.handleLifecycleEvent(event) + } + } + else -> { + visibleLifecycleOwner.handleLifecycleEvent(event) + } + } + } + }) } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) - if (!hidden) { - viewModel.clearCapsules() + if (hidden) { + onHidden() + } else { + onShow() } } + private fun onHidden() { + viewModel.viewModelReload = false + reloadState = true + } + + private fun onShow() { + visibleLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START) + if (reload){ + viewModel.load() + reload = false + viewModel.viewModelReload = false + binding.myPageRV.scrollToPosition(0) + } + if (!reload && !reloadState){ + viewModel.load() + reload = false + viewModel.viewModelReload = false + binding.myPageRV.scrollToPosition(0) + } + + if (reloadMyInfo){ + viewModel.getMe() + } + } + + override fun onResume() { + super.onResume() + reloadState = false + reload = false + } + companion object { const val TAG = "MY" - fun newIntent() = MyPageFragment() + const val PROFILE_TYPE = 1 + const val STORY_TYPE = 2 + const val SPINNER_TYPE = 3 + const val CAPSULE_TYPE = 4 + + private var reloadState = true + private var reload = false + fun newIntent() : MyPageFragment{ + if (reloadState){ + reload = true + } + return MyPageFragment() + } + } + + enum class SpinnerCapsuleType(val description: String) { + SECRET("Secret"), + PUBLIC("Public"), + GROUP("Group") } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModel.kt index b459cd493..c09355fae 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModel.kt @@ -1,9 +1,7 @@ package com.droidblossom.archive.presentation.ui.mypage -import com.droidblossom.archive.domain.model.common.MyCapsule import com.droidblossom.archive.domain.model.member.MemberDetail -import com.droidblossom.archive.presentation.base.BaseViewModel -import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleViewModel +import com.droidblossom.archive.presentation.model.mypage.CapsuleData import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -11,20 +9,32 @@ interface MyPageViewModel { val myPageEvents : SharedFlow val myInfo : StateFlow - val myCapsules : StateFlow> - val myCapsulesUI : StateFlow> + val myCapsules : StateFlow> + val myCapsulesUI : StateFlow> val hasNextPage : StateFlow val lastCreatedTime : StateFlow + val capsuleType: StateFlow + var viewModelReload:Boolean + var clearCapsule:Boolean fun getMe() - fun getSecretCapsulePage() - fun clearCapsules() - + fun clearCapsules(setting:Boolean) fun updateMyCapsulesUI() + fun updateCapsuleOpenState(capsuleIndex: Int, capsuleId: Long) + fun clickSetting() - fun updateCapsuleOpenState(capsuleId: Long, isOpened: Boolean) + fun getCapsulePage() + fun load() + fun selectSpinnerItem(item:MyPageFragment.SpinnerCapsuleType) + fun myPageEvent(event: MyPageEvent) + fun onScrollNearBottom() sealed class MyPageEvent { data class ShowToastMessage(val message : String) : MyPageEvent() + + object HideLoading : MyPageEvent() + + object ClickSetting : MyPageEvent() + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModelImpl.kt index 0bddd9a64..b18de5f0e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/MyPageViewModelImpl.kt @@ -2,49 +2,51 @@ package com.droidblossom.archive.presentation.ui.mypage import android.util.Log import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.domain.model.common.MyCapsule +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.domain.model.member.MemberDetail -import com.droidblossom.archive.domain.model.secret.SecretCapsulePageRequest import com.droidblossom.archive.domain.usecase.member.MemberUseCase +import com.droidblossom.archive.domain.usecase.open.MyPublicCapsulePageUseCase import com.droidblossom.archive.domain.usecase.secret.SecretCapsulePageUseCase import com.droidblossom.archive.presentation.base.BaseViewModel -import com.droidblossom.archive.presentation.ui.auth.AuthViewModel +import com.droidblossom.archive.presentation.model.mypage.CapsuleData import com.droidblossom.archive.util.DateUtils import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel class MyPageViewModelImpl @Inject constructor( private val memberUseCase: MemberUseCase, - private val secretCapsulePageUseCase: SecretCapsulePageUseCase + private val secretCapsulePageUseCase: SecretCapsulePageUseCase, + private val myPublicCapsulePageUseCase: MyPublicCapsulePageUseCase ) : BaseViewModel(), MyPageViewModel { private val _myPageEvents = MutableSharedFlow() override val myPageEvents: SharedFlow - get() =_myPageEvents.asSharedFlow() + get() = _myPageEvents.asSharedFlow() - private val _myInfo = MutableStateFlow(MemberDetail("USER", "", "")) + private val _myInfo = MutableStateFlow(MemberDetail("USER", "", "", 0, 0)) override val myInfo: StateFlow get() = _myInfo - private val _myCapsules = MutableStateFlow(listOf()) - override val myCapsules: StateFlow> + private val _myCapsules = MutableStateFlow(listOf()) + override val myCapsules: StateFlow> get() = _myCapsules - private val _myCapsulesUI = MutableStateFlow(listOf()) - override val myCapsulesUI: StateFlow> + private val _myCapsulesUI = MutableStateFlow(listOf()) + override val myCapsulesUI: StateFlow> get() = _myCapsulesUI private val _hasNextPage = MutableStateFlow(true) @@ -54,6 +56,44 @@ class MyPageViewModelImpl @Inject constructor( override val lastCreatedTime: StateFlow get() = _lastCreatedTime + private val _capsuleType = MutableStateFlow(MyPageFragment.SpinnerCapsuleType.SECRET) + override val capsuleType: StateFlow + get() = _capsuleType + + private val scrollEventChannel = Channel(Channel.CONFLATED) + private val scrollEventFlow = + scrollEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) + + private var getCapsuleListJob: Job? = null + + override var viewModelReload = false + override var clearCapsule = false + + init { + load() + viewModelScope.launch { + scrollEventFlow.collect { + getCapsulePage() + } + } + } + + override fun load() { + getMe() + clearCapsules(true) + } + + override fun onScrollNearBottom() { + scrollEventChannel.trySend(Unit) + } + + + override fun myPageEvent(event: MyPageViewModel.MyPageEvent) { + viewModelScope.launch { + _myPageEvents.emit(event) + } + } + override fun getMe() { viewModelScope.launch { @@ -61,60 +101,123 @@ class MyPageViewModelImpl @Inject constructor( result.onSuccess { _myInfo.emit(it) }.onFail { - _myPageEvents.emit(MyPageViewModel.MyPageEvent.ShowToastMessage("정보 불러오기 실패")) + myPageEvent(MyPageViewModel.MyPageEvent.ShowToastMessage("정보 불러오기 실패")) } } } } - override fun getSecretCapsulePage() { - viewModelScope.launch { - if (hasNextPage.value) { + override fun getCapsulePage() { + when (capsuleType.value) { + MyPageFragment.SpinnerCapsuleType.SECRET -> { + getSecretCapsulePage() + } + + MyPageFragment.SpinnerCapsuleType.PUBLIC -> { + getPublicCapsulePage() + } + + MyPageFragment.SpinnerCapsuleType.GROUP -> { + getGroupCapsulePage() + } + } + } + + private fun getSecretCapsulePage() { + if (hasNextPage.value) { + getCapsuleListJob?.cancel() + getCapsuleListJob = viewModelScope.launch { secretCapsulePageUseCase( - SecretCapsulePageRequest( + PagingRequestDto( 15, lastCreatedTime.value - ).toDto() + ) ).collect { result -> result.onSuccess { _hasNextPage.value = it.hasNext _myCapsules.emit(myCapsules.value + it.capsules) - _lastCreatedTime.value = _myCapsules.value.last().createdDate + if (myCapsules.value.isNotEmpty()) { + _lastCreatedTime.value = myCapsules.value.last().createdDate + } }.onFail { - _myPageEvents.emit(MyPageViewModel.MyPageEvent.ShowToastMessage("정보 불러오기 실패")) + myPageEvent(MyPageViewModel.MyPageEvent.ShowToastMessage("정보 불러오기 실패")) } } + myPageEvent(MyPageViewModel.MyPageEvent.HideLoading) } } } + private fun getPublicCapsulePage() { + if (hasNextPage.value) { + getCapsuleListJob?.cancel() + getCapsuleListJob = viewModelScope.launch { + myPublicCapsulePageUseCase( + PagingRequestDto( + 15, + lastCreatedTime.value + ) + ).collect { result -> + result.onSuccess { + _hasNextPage.value = it.hasNext + _myCapsules.emit(myCapsules.value + it.capsules) + if (myCapsules.value.isNotEmpty()) { + _lastCreatedTime.value = myCapsules.value.last().createdDate + } + }.onFail { + myPageEvent(MyPageViewModel.MyPageEvent.ShowToastMessage("정보 불러오기 실패")) + } + } + myPageEvent(MyPageViewModel.MyPageEvent.HideLoading) + } + } + } + + private fun getGroupCapsulePage() { + viewModelScope.launch { + if (hasNextPage.value) { + _myCapsules.emit(listOf()) + myPageEvent(MyPageViewModel.MyPageEvent.HideLoading) + } + } + } + + override fun updateMyCapsulesUI() { viewModelScope.launch { _myCapsulesUI.emit(myCapsules.value) } } - override fun clearCapsules() { + override fun clearCapsules(setting: Boolean) { + clearCapsule = setting viewModelScope.launch { _myCapsules.value = listOf() _lastCreatedTime.value = DateUtils.dataServerString _hasNextPage.value = true - getSecretCapsulePage() + getCapsulePage() } } - override fun updateCapsuleOpenState(capsuleId: Long, isOpened: Boolean) { + override fun updateCapsuleOpenState(capsuleIndex: Int, capsuleId: Long) { viewModelScope.launch { - val updatedCapsules = withContext(Dispatchers.Default) { - _myCapsules.value.map { capsule -> - if (capsule.capsuleId == capsuleId) { - capsule.copy(isOpened = isOpened) - } else { - capsule - } - } - } - _myCapsules.emit(updatedCapsules) + val newList = _myCapsules.value + newList[capsuleIndex].isOpened = true + _myCapsules.emit(newList) + _myCapsulesUI.emit(myCapsules.value) + } + } + + override fun clickSetting() { + viewModelScope.launch { + myPageEvent(MyPageViewModel.MyPageEvent.ClickSetting) + } + } + + override fun selectSpinnerItem(item: MyPageFragment.SpinnerCapsuleType) { + if (capsuleType.value != item){ + viewModelReload = true + _capsuleType.value = item } } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/CapsuleRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/CapsuleRVA.kt new file mode 100644 index 000000000..99c76f4ff --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/CapsuleRVA.kt @@ -0,0 +1,78 @@ +package com.droidblossom.archive.presentation.ui.mypage.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemMyPageCapsuleBinding +import com.droidblossom.archive.presentation.ui.mypage.MyPageFragment.Companion.CAPSULE_TYPE +import com.droidblossom.archive.presentation.model.mypage.CapsuleData +import com.droidblossom.archive.presentation.ui.home.HomeFragment +import com.droidblossom.archive.util.CapsuleTypeUtils + +class CapsuleRVA( + private val goDetail: (Long, HomeFragment.CapsuleType) -> Unit, + private val goSummary: (Int, Long, HomeFragment.CapsuleType) -> Unit +) : + ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemMyPageCapsuleBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: CapsuleData) { + binding.data = data + binding.root.setOnClickListener { + if (data.isOpened) { + goDetail(data.capsuleId, CapsuleTypeUtils.stringToEnum(data.capsuleType)) + } else { + goSummary(bindingAdapterPosition, data.capsuleId, + CapsuleTypeUtils.stringToEnum(data.capsuleType) + ) + } + } + } + } + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long { + return getItem(position).capsuleId + } + + + override fun getItemViewType(position: Int): Int { + return CAPSULE_TYPE + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemMyPageCapsuleBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CapsuleData, newItem: CapsuleData): Boolean { + return oldItem.capsuleId == newItem.capsuleId + } + + override fun areContentsTheSame(oldItem: CapsuleData, newItem: CapsuleData): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/CapsuleTypeSpinner.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/CapsuleTypeSpinner.kt new file mode 100644 index 000000000..1c578bc53 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/CapsuleTypeSpinner.kt @@ -0,0 +1,75 @@ +package com.droidblossom.archive.presentation.ui.mypage.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView +import android.widget.BaseAdapter +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ItemSpinnerBinding +import com.droidblossom.archive.databinding.ItemSpinnerDropdownBinding +import com.droidblossom.archive.presentation.ui.mypage.MyPageFragment + +class CapsuleTypeSpinner(private val context: Context, private val items: Array) : BaseAdapter() { + + var spinnerIsOpened = false + private lateinit var spinnerItemBinding: ItemSpinnerBinding + private var selectedItemDescription: String? = null + + override fun getCount(): Int = items.size + + override fun getItem(position: Int): MyPageFragment.SpinnerCapsuleType = items[position] + + override fun getItemId(position: Int): Long = position.toLong() + + @SuppressLint("ViewHolder") + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + spinnerItemBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_spinner, parent, false) + spinnerItemBinding.spinnerItemName.text = getItem(position).description + selectedItemDescription = getItem(position).description + return spinnerItemBinding.root + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + + if (spinnerIsOpened) { + spinnerItemBinding.spinnerItemName.setBackgroundResource(R.drawable.spinner_background_close) + } else { + spinnerItemBinding.spinnerItemName.setBackgroundResource(R.drawable.spinner_background_open) + } + + val isLastVisibleItem = (position == items.indices.last { getItem(it).description != selectedItemDescription }) + + if (getItem(position).description == selectedItemDescription) { + val view = convertView ?: View(context) + view.visibility = View.GONE + return view + } else { + val binding: ItemSpinnerDropdownBinding = if (convertView == null || convertView.tag == null) { + DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_spinner_dropdown, parent, false) + } else { + convertView.tag as ItemSpinnerDropdownBinding + } + + binding.dropdownItemName.text = getItem(position).description + + if (isLastVisibleItem) { + binding.root.setBackgroundResource(R.drawable.spinner_dropdown_item_background_last) + } else { + binding.root.setBackgroundResource(R.drawable.spinner_dropdown_item_background) + } + + if (getItem(position).description != selectedItemDescription) { + binding.dropdownItemName.setTextColor(ContextCompat.getColor(context, R.color.gray_300)) + } + + binding.root.tag = binding + binding.root.visibility = View.VISIBLE + return binding.root + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/MyCapsuleRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/MyCapsuleRVA.kt deleted file mode 100644 index 988bfd1a8..000000000 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/MyCapsuleRVA.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.droidblossom.archive.presentation.ui.mypage.adapter - -import android.graphics.Color -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.annotation.ColorRes -import androidx.core.content.res.ResourcesCompat.getColor -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.amazonaws.util.ClassLoaderHelper.getResource -import com.droidblossom.archive.R -import com.droidblossom.archive.databinding.ItemMyCapsuleBinding -import com.droidblossom.archive.domain.model.common.MyCapsule -import com.droidblossom.archive.presentation.ui.home.HomeFragment -import com.droidblossom.archive.util.CapsuleTypeUtils.stringToEnum - -class MyCapsuleRVA( - private val goDetail: (Long, HomeFragment.CapsuleType) -> Unit, - private val goSummary: (Long, HomeFragment.CapsuleType) -> Unit -) : - ListAdapter(differ) { - - inner class ItemViewHolder( - private val binding: ItemMyCapsuleBinding - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(data: MyCapsule) { - binding.item = data - binding.root.setOnClickListener { - if (data.isOpened) { - goDetail(data.capsuleId, stringToEnum(data.capsuleType)) - } else { - goSummary(data.capsuleId, stringToEnum(data.capsuleType)) - } - - } - } - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): ItemViewHolder { - return ItemViewHolder( - ItemMyCapsuleBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - companion object { - val differ = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MyCapsule, newItem: MyCapsule): Boolean { - return oldItem.capsuleId == newItem.capsuleId - } - - override fun areContentsTheSame(oldItem: MyCapsule, newItem: MyCapsule): Boolean { - return oldItem == newItem - } - } - } -} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/ProfileRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/ProfileRVA.kt new file mode 100644 index 000000000..0dd889135 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/ProfileRVA.kt @@ -0,0 +1,73 @@ +package com.droidblossom.archive.presentation.ui.mypage.adapter + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemMyPageProfileBinding +import com.droidblossom.archive.presentation.ui.mypage.MyPageFragment.Companion.PROFILE_TYPE +import com.droidblossom.archive.presentation.model.mypage.ProfileData + +class ProfileRVA( + private val goGroupList: () -> Unit, + private val goFriendList: () -> Unit, + private val goRequestList: () -> Unit, + private val goSetting: () -> Unit, +) : ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemMyPageProfileBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: ProfileData) { + binding.data = data + binding.groupLayout.setOnClickListener { goGroupList() } + binding.friendLayout.setOnClickListener { goFriendList() } + binding.requestLayout.setOnClickListener { goRequestList() } + binding.settingBtn.setOnClickListener { goSetting() } + } + } + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long { + return getItem(position).profileId + } + + override fun getItemViewType(position: Int): Int { + return PROFILE_TYPE + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemMyPageProfileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ProfileData, newItem: ProfileData): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: ProfileData, newItem: ProfileData): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/SpinnerAdapter.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/SpinnerAdapter.kt new file mode 100644 index 000000000..cff5b2c7d --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/SpinnerAdapter.kt @@ -0,0 +1,75 @@ +package com.droidblossom.archive.presentation.ui.mypage.adapter + +import android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemMyPageSpinnerBinding +import com.droidblossom.archive.presentation.ui.mypage.MyPageFragment + +class SpinnerAdapter( + private val context: Context, + private val spinnerItems: Array, + private val selectedCapsuleType: (MyPageFragment.SpinnerCapsuleType) -> Unit +) : RecyclerView.Adapter() { + + private var selectedPosition = 0 + + inner class ItemViewHolder( + private val binding: ItemMyPageSpinnerBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(spinnerItems: Array, position: Int) { + val spinnerAdapter = CapsuleTypeSpinner(context, spinnerItems) + binding.capsuleTypeSpinner.adapter = spinnerAdapter + binding.capsuleTypeSpinner.setSelection(selectedPosition, false) + binding.capsuleTypeSpinner.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View, + position: Int, + id: Long + ) { + selectedCapsuleType(spinnerItems[position]) + selectedPosition = position + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + + } + } + binding.capsuleTypeSpinner.viewTreeObserver.addOnWindowFocusChangeListener { hasFocus -> + spinnerAdapter.spinnerIsOpened = hasFocus + spinnerAdapter.notifyDataSetChanged() + } + } + } + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long = position.toLong() + + override fun getItemViewType(position: Int): Int = MyPageFragment.SPINNER_TYPE + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder( + ItemMyPageSpinnerBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(spinnerItems, position) + } + + override fun getItemCount(): Int = 1 +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/StoryRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/StoryRVA.kt new file mode 100644 index 000000000..425ab9eb1 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/adapter/StoryRVA.kt @@ -0,0 +1,56 @@ +package com.droidblossom.archive.presentation.ui.mypage.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemMyPageStoryBinding +import com.droidblossom.archive.presentation.model.mypage.StoryData + +class StoryRVA( + +) : ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemMyPageStoryBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: StoryData) { + binding.data = data + binding.root.setOnClickListener { + + + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemMyPageStoryBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: StoryData, newItem: StoryData): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: StoryData, newItem: StoryData): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendActivity.kt new file mode 100644 index 000000000..0955a631b --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendActivity.kt @@ -0,0 +1,112 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.viewModels +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivityFriendBinding +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.ui.mypage.friend.adapter.FriendVPA +import com.droidblossom.archive.presentation.ui.mypage.friend.addfriend.AddFriendActivity +import com.droidblossom.archive.presentation.ui.mypage.friend.addgroup.AddGroupActivity +import com.droidblossom.archive.presentation.ui.mypage.friend.addgroup.AddGroupViewModelImpl +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import com.google.android.material.tabs.TabLayoutMediator +import com.kakao.sdk.common.KakaoSdk +import com.kakao.sdk.common.KakaoSdk.type +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class FriendActivity : + BaseActivity(R.layout.activity_friend) { + override val viewModel: FriendViewModelImpl by viewModels() + + private val friendVPA by lazy { + FriendVPA(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initView() + + viewModel.getFriendList() + } + + private fun initView() { + val layoutParams = binding.closeBtn.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.closeBtn.layoutParams = layoutParams + + binding.vp.adapter = friendVPA + + binding.addCV.setOnClickListener { + startActivity(AddGroupActivity.newIntent(this@FriendActivity)) + } + + binding.closeBtn.setOnClickListener { + finish() + } + + TabLayoutMediator(binding.tab, binding.vp) { tab, position -> + tab.text = when (position) { + 0 -> getString(R.string.groupList) + 1 -> getString(R.string.friendList) + else -> null + } + }.attach() + + binding.tab.addOnTabSelectedListener(object : OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + if (tab.position == 0) { + binding.addT.text = "그룹 추가" + binding.addCV.setOnClickListener { + startActivity(AddGroupActivity.newIntent(this@FriendActivity)) + } + } else { + binding.addT.text = "친구 추가" + binding.addCV.setOnClickListener { + startActivity(AddFriendActivity.newIntent(this@FriendActivity)) + } + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) {} + override fun onTabReselected(tab: TabLayout.Tab) {} + }) + + intent.getStringExtra(TYPE_KEY)?.let { type -> + when (type) { + GROUP -> { + binding.tab.getTabAt(0)?.select() + } + + FRIEND -> { + binding.tab.getTabAt(1)?.select() + } + + else -> { + binding.tab.getTabAt(0)?.select() + } + + } + } + } + + override fun observeData() { + + } + + companion object { + const val FRIEND = "friend" + const val GROUP = "group" + const val TYPE_KEY = "type_key" + fun newIntent(context: Context, type: String) = + Intent(context, FriendActivity::class.java).apply { + putExtra(TYPE_KEY, type) + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendViewModel.kt new file mode 100644 index 000000000..99049c618 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendViewModel.kt @@ -0,0 +1,38 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend + +import com.droidblossom.archive.domain.model.friend.Friend +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse +import com.droidblossom.archive.presentation.ui.mypage.friend.addfriend.AddFriendViewModel +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface FriendViewModel { + + val friendEvent: SharedFlow + + //friend + val isFriendSearchOpen: StateFlow + val friendList: StateFlow> + val friendListUI: StateFlow> + + //group + val isGroupSearchOpen: StateFlow + fun onScrollNearBottomFriend() + + //friend + fun openSearchFriend() + fun closeSearchFriend() + fun searchFriend() + fun getFriendList() + fun changeDeleteOpen(previousPosition: Int?, currentPosition: Int) + fun deleteFriend(friend: Friend) + + //group + fun openSearchGroup() + fun closeSearchGroup() + fun searchGroup() + + sealed class FriendEvent { + data class ShowToastMessage(val message: String) : FriendViewModel.FriendEvent() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendViewModelImpl.kt new file mode 100644 index 000000000..52e69e99b --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/FriendViewModelImpl.kt @@ -0,0 +1,176 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend + +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.friend.Friend +import com.droidblossom.archive.domain.usecase.friend.FriendDeleteUseCase +import com.droidblossom.archive.domain.usecase.friend.FriendsPageUseCase +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.presentation.base.BaseViewModel.Companion.throttleFirst +import com.droidblossom.archive.presentation.ui.home.notification.NotificationViewModel +import com.droidblossom.archive.presentation.ui.mypage.friend.addfriend.AddFriendViewModel +import com.droidblossom.archive.util.DateUtils +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class FriendViewModelImpl @Inject constructor( + private val friendsPageUseCase: FriendsPageUseCase, + private val friendDeleteUseCase: FriendDeleteUseCase +) : BaseViewModel(), FriendViewModel { + + private val _friendEvent = MutableSharedFlow() + override val friendEvent: SharedFlow + get() = _friendEvent.asSharedFlow() + + //friend + private val _isFriendSearchOpen = MutableStateFlow(false) + + override val isFriendSearchOpen: StateFlow + get() = _isFriendSearchOpen + + + private val _friendListUI = MutableStateFlow>(listOf()) + override val friendListUI: StateFlow> + get() = _friendListUI + + private val _friendList = MutableStateFlow>(listOf()) + override val friendList: StateFlow> + get() = _friendList + + private val friendHasNextPage = MutableStateFlow(true) + + private val friendLastCreatedTime = MutableStateFlow(DateUtils.dataServerString) + + //group + private val _isGroupSearchOpen = MutableStateFlow(false) + + override val isGroupSearchOpen: StateFlow + get() = _isGroupSearchOpen + + private val scrollFriendEventChannel = Channel(Channel.CONFLATED) + private val scrollFriendEventFlow = + scrollFriendEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) + + private var getFriendLstJob: Job? = null + + init { + viewModelScope.launch { + scrollFriendEventFlow.collect { + getFriendList() + } + } + } + + override fun onScrollNearBottomFriend() { + scrollFriendEventChannel.trySend(Unit) + } + + //friend + override fun openSearchFriend() { + viewModelScope.launch { + _isFriendSearchOpen.emit(true) + } + } + + override fun closeSearchFriend() { + viewModelScope.launch { + _isFriendSearchOpen.emit(false) + } + } + + override fun searchFriend() { + + } + + override fun getFriendList() { + if (friendHasNextPage.value) { + getFriendLstJob?.cancel() + getFriendLstJob = viewModelScope.launch { + friendsPageUseCase( + PagingRequestDto( + 15, + friendLastCreatedTime.value + ) + ).collect { result -> + result.onSuccess { + friendHasNextPage.value = it.hasNext + if (friendListUI.value.isEmpty()) { + _friendListUI.emit(it.friends) + _friendList.emit(it.friends) + } else { + _friendListUI.emit(_friendListUI.value + it.friends) + _friendList.emit(friendList.value + it.friends) + } + friendLastCreatedTime.value = it.friends.last().createdAt + }.onFail { + _friendEvent.emit( + FriendViewModel.FriendEvent.ShowToastMessage( + "친구 리스트 불러오기 실패. 잠시후 시도해 주세요" + ) + ) + } + } + } + } + } + + override fun changeDeleteOpen(previousPosition: Int?, currentPosition: Int) { + viewModelScope.launch { + val newList = _friendListUI.value + previousPosition?.let { + newList[it].isOpenDelete = false + } + newList[currentPosition].isOpenDelete = true + _friendListUI.emit(newList) + } + } + + override fun deleteFriend(friend: Friend) { + viewModelScope.launch { + friendDeleteUseCase(friend.id).collect { result -> + result.onSuccess { + val list = friendListUI.value.toMutableList() + list.remove(friend) + _friendListUI.emit(list) + }.onFail { + _friendEvent.emit( + FriendViewModel.FriendEvent.ShowToastMessage( + "친구 삭제 실패. 잠시후 시도해 주세요" + ) + ) + } + } + } + } + + //group + override fun openSearchGroup() { + viewModelScope.launch { + _isGroupSearchOpen.emit(true) + } + } + + override fun closeSearchGroup() { + viewModelScope.launch { + _isGroupSearchOpen.emit(false) + } + } + + override fun searchGroup() { + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/adapter/FriendRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/adapter/FriendRVA.kt new file mode 100644 index 000000000..2ef93e567 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/adapter/FriendRVA.kt @@ -0,0 +1,103 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.gesture.Gesture +import android.gesture.GestureOverlayView +import android.util.Log +import android.view.GestureDetector +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemFriendBinding +import com.droidblossom.archive.domain.model.friend.Friend + + +class FriendRVA( + private val context: Context, + private val click: (Int?, Int) -> Unit, + private val delete: (Friend) -> Unit +) : + ListAdapter(differ) { + + private var previousClickedPosition: Int? = null + + inner class ItemViewHolder( + private val binding: ItemFriendBinding + ) : RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("ClickableViewAccessibility") + fun bind(data: Friend) { + binding.item = data + + val detector = GestureDetector(context, object : GestureDetector.OnGestureListener { + override fun onDown(p0: MotionEvent) = true + override fun onShowPress(p0: MotionEvent) = Unit + override fun onSingleTapUp(p0: MotionEvent) = true + override fun onScroll( + p0: MotionEvent?, p1: MotionEvent, + p2: Float, p3: Float + ): Boolean { + val currentClickedPosition = bindingAdapterPosition + if (currentClickedPosition != RecyclerView.NO_POSITION) { + click(previousClickedPosition, currentClickedPosition) + previousClickedPosition?.let { previousClickedPosition -> + notifyItemChanged(previousClickedPosition) + } + notifyItemChanged(currentClickedPosition) + previousClickedPosition = currentClickedPosition + } + Log.d("아이템 ", "onScroll() called : $p0") + return true + } + + override fun onLongPress(p0: MotionEvent) = Unit + override fun onFling(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float) = true + }) + binding.root.setOnTouchListener { _, event -> + detector.onTouchEvent(event) + return@setOnTouchListener false + } + + binding.deleteBtn.setOnClickListener { + delete(data) + notifyItemRemoved(bindingAdapterPosition) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder( + ItemFriendBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Friend, + newItem: Friend + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: Friend, + newItem: Friend + ): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/adapter/FriendVPA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/adapter/FriendVPA.kt new file mode 100644 index 000000000..1483f4595 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/adapter/FriendVPA.kt @@ -0,0 +1,21 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.droidblossom.archive.presentation.ui.mypage.friend.page.FriendListFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.page.GroupListFragment + +class FriendVPA(activity: FragmentActivity) : FragmentStateAdapter(activity){ + + private val fragmentList = listOf( + GroupListFragment(), + FriendListFragment() + ) + + override fun getItemCount(): Int = fragmentList.size + + override fun createFragment(position: Int): Fragment { + return fragmentList[position] + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendActivity.kt new file mode 100644 index 000000000..38302b76c --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendActivity.kt @@ -0,0 +1,41 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addfriend + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.activity.viewModels +import androidx.core.content.ContentProviderCompat.requireContext +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivityAddFriendBinding +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.util.ContactsUtils +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AddFriendActivity : BaseActivity(R.layout.activity_add_friend) { + + override val viewModel: AddFriendViewModelImpl by viewModels() + + override fun observeData() {} + +// override fun onRequestPermissionsResult( +// requestCode: Int, +// permissions: Array, +// grantResults: IntArray +// ) { +// super.onRequestPermissionsResult(requestCode, permissions, grantResults) +// +// if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { +// //viewModel.contactsSearch(ContactsUtils.getContacts(this)) +// } else { +// //showToastMessage("권한이 필요합니다.") +// } +// } + + companion object { + const val ADDFRIEND = "add_friend" + + fun newIntent(context: Context) = + Intent(context, AddFriendActivity::class.java) + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendViewModel.kt new file mode 100644 index 000000000..dea7de6ea --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendViewModel.kt @@ -0,0 +1,37 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addfriend + +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse +import com.droidblossom.archive.presentation.ui.mypage.MyPageViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface AddFriendViewModel { + + val addEvent : SharedFlow + + //searchName + val addFriendListUI : StateFlow> + val checkedList : StateFlow> + val tagT : MutableStateFlow + + //searchNum + val isSearchNumOpen : StateFlow + val addFriendList: StateFlow> + + + fun requestFriends() + //searchName + fun searchTag() + + //searchNum + fun searchNum() + fun openSearchNum() + + sealed class AddEvent { + data class ShowToastMessage(val message : String) : AddEvent() + object OpenLoading : AddEvent() + object CloseLoading : AddEvent() + object NotificationChange : AddEvent() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendViewModelImpl.kt new file mode 100644 index 000000000..aa1b4faf7 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/AddFriendViewModelImpl.kt @@ -0,0 +1,174 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addfriend + +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.data.dto.friend.request.PhoneBooks +import com.droidblossom.archive.domain.model.friend.FriendReqRequest +import com.droidblossom.archive.domain.model.friend.FriendsSearchPhoneRequest +import com.droidblossom.archive.domain.model.friend.FriendsSearchRequest +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse +import com.droidblossom.archive.domain.usecase.friend.FriendListRequestUseCase +import com.droidblossom.archive.domain.usecase.friend.FriendsRequestUseCase +import com.droidblossom.archive.domain.usecase.friend.FriendsSearchPhoneUseCase +import com.droidblossom.archive.domain.usecase.friend.FriendsSearchUseCase +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AddFriendViewModelImpl @Inject constructor( + private val friendsSearchUseCase: FriendsSearchUseCase, + private val friendsRequestUseCase: FriendsRequestUseCase, + private val friendListRequestUseCase: FriendListRequestUseCase, + private val friendsSearchPhoneUseCase: FriendsSearchPhoneUseCase +) : BaseViewModel(), AddFriendViewModel { + + private val _addEvent = MutableSharedFlow() + + override val addEvent: SharedFlow + get() = _addEvent.asSharedFlow() + + //name + + private val _addFriendListUI = MutableStateFlow>(listOf()) + override val addFriendListUI: StateFlow> + get() = _addFriendListUI + + private val _addFriendList = MutableStateFlow>(listOf()) + override val addFriendList: StateFlow> + get() = _addFriendList + + + private val _checkedList = MutableStateFlow>(listOf()) + override val checkedList: StateFlow> + get() = _checkedList + + override val tagT: MutableStateFlow = MutableStateFlow("") + + + //Num + + private val _isSearchNumOpen = MutableStateFlow(false) + override val isSearchNumOpen: StateFlow + get() = _isSearchNumOpen + + + //Name + override fun requestFriends() { + viewModelScope.launch { + checkedList.value.forEach { friend -> + friendsRequestUseCase(FriendReqRequest(friendId = friend.id)).collect { result -> + result.onSuccess { + _addEvent.emit(AddFriendViewModel.AddEvent.ShowToastMessage("친구 요청을 보냈습니다")) + }.onFail { + _addEvent.emit(AddFriendViewModel.AddEvent.ShowToastMessage("친구 요청 오류 발생")) + } + } + changeRequestUI(friend.id) + } + _checkedList.emit(listOf()) + _addEvent.emit(AddFriendViewModel.AddEvent.NotificationChange) + + } + } + + fun checkAddFriendList(position: Int) { + viewModelScope.launch { + val newAddList = addFriendListUI.value + if (newAddList[position].isChecked) { + newAddList[position].isChecked = false + _checkedList.emit(newAddList.filter { it.isChecked }) + } else { + newAddList[position].isChecked = true + _checkedList.emit(newAddList.filter { it.isChecked }) + } + _addFriendListUI.emit(newAddList) + } + } + + override fun searchTag() { + viewModelScope.launch { + friendsSearchUseCase(FriendsSearchRequest(tagT.value)).collect { result -> + result.onSuccess { response -> + _addFriendListUI.emit(listOf(response)) + }.onFail { + _addEvent.emit(AddFriendViewModel.AddEvent.ShowToastMessage("검색이 불가능 합니다.")) + } + } + } + } + + fun resetList() { + viewModelScope.launch { + if (checkedList.value.isNotEmpty()) { + _checkedList.emit(listOf()) + } + if (addFriendListUI.value.isNotEmpty()) { + _addFriendListUI.emit(listOf()) + } + } + } + + + //Num + override fun searchNum() { + //respone에 이름 추가시 구현 + } + + override fun openSearchNum() { + viewModelScope.launch { + _isSearchNumOpen.emit(true) + } + } + + fun closeSearchNum() { + viewModelScope.launch { + _isSearchNumOpen.emit(false) + } + } + + fun contactsSearch(phoneBooks: List) { + viewModelScope.launch { + _addEvent.emit(AddFriendViewModel.AddEvent.OpenLoading) + friendsSearchPhoneUseCase(FriendsSearchPhoneRequest(phoneBooks.filter { + it.originPhone.length == 11 && it.originPhone.substring(0, 3) == "010" + })).collect { result -> + result.onSuccess { response -> + _addFriendList.emit(response.friends) + _addFriendListUI.emit(response.friends) + _addEvent.emit(AddFriendViewModel.AddEvent.CloseLoading) + }.onFail { + _addEvent.emit(AddFriendViewModel.AddEvent.ShowToastMessage("주소록 불러오기 실패.")) + _addEvent.emit(AddFriendViewModel.AddEvent.CloseLoading) + } + } + } + } + + fun closeLoading() { + viewModelScope.launch { + _addEvent.emit(AddFriendViewModel.AddEvent.CloseLoading) + } + } + + private fun changeRequestUI(id: Long) { + val newList = addFriendListUI.value + val index = newList.indexOfFirst { + it.id == id + } + if (index != -1) { + newList[index].isFriendInviteToFriend = true + newList[index].isChecked = false + viewModelScope.launch { + _addFriendListUI.emit(newList) + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/SearchFriendNicknameFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/SearchFriendNicknameFragment.kt new file mode 100644 index 000000000..07af7cabb --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/SearchFriendNicknameFragment.kt @@ -0,0 +1,217 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addfriend + +import android.Manifest +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentFriendSearchNicknameBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment +import com.droidblossom.archive.presentation.ui.camera.CameraFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.addfriend.adapter.AddFriendRVA +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SearchFriendNicknameFragment : + BaseFragment(R.layout.fragment_friend_search_nickname) { + + override val viewModel: AddFriendViewModelImpl by viewModels() + + lateinit var navController: NavController + + private val addFriendRVA by lazy { + AddFriendRVA { position -> + viewModel.checkAddFriendList(position) + } + } + + private val permissionsToRequest: Array by lazy { + val permissionsList = mutableListOf() + permissionsList.add(Manifest.permission.READ_CONTACTS) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + permissionsList.add(Manifest.permission.READ_PHONE_NUMBERS) + } + + permissionsList.toTypedArray() + } + + private val requestContactsCallPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + navController.navigate(R.id.action_searchFriendNicknameFragment_to_searchFriendNumberFragment) + } + permissions.none { it.value } -> { + handleAllPermissionsDenied() + } + else -> { + handlePartialPermissionsDenied(permissions) + } + } + } + + private fun handleAllPermissionsDenied() { + if (permissionsToRequest.size > 1) { + if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS) || + shouldShowRequestPermissionRationale(Manifest.permission.READ_PHONE_NUMBERS) + ) { + showToastMessage("앱에서 친구를 찾기 위해 연락처, 전화 접근 권한이 필요합니다.") + } else { + showSettingsDialog( + PermissionDialogFragment.PermissionType.CONTACTS_AND_CALL, object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("앱에서 친구를 찾기 위해 연락처, 전화 접근 권한이 필요합니다.") + } + + override fun onRightButtonClicked() { + navigateToAppSettings { + requestContactsCallPermissionLauncher.launch(permissionsToRequest) + } + } + } + ) + } + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { + showToastMessage("앱에서 친구를 찾기 위해 연락처 접근 권한이 필요합니다.") + } else { + showSettingsDialog(PermissionDialogFragment.PermissionType.CONTACTS, object : PermissionDialogButtonClickListener{ + override fun onLeftButtonClicked() { + showToastMessage("앱에서 친구를 찾기 위해 연락처 접근 권한이 필요합니다.") + } + + override fun onRightButtonClicked() { + navigateToAppSettings{ + requestContactsCallPermissionLauncher.launch(permissionsToRequest) + } + } + }) + } + } + } + + private fun handlePartialPermissionsDenied(permissions: Map) { + permissions.forEach { (perm, granted) -> + if (!granted) { + when (perm) { + Manifest.permission.READ_CONTACTS -> + showPermissionDialog(PermissionDialogFragment.PermissionType.CONTACTS) + + Manifest.permission.READ_PHONE_NUMBERS -> + showPermissionDialog(PermissionDialogFragment.PermissionType.CALL) + } + } + } + } + + private fun showPermissionDialog(permissionType: PermissionDialogFragment.PermissionType) { + if (shouldShowRequestPermissionRationale(permissionType.toString())) { + showToastMessage("앱에서 친구를 찾기 위해 ${permissionType.description} 권한이 필요합니다.") + } else { + showSettingsDialog(permissionType, object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("앱에서 친구를 찾기 위해 ${permissionType.description} 권한이 필요합니다.") + } + + override fun onRightButtonClicked() { + navigateToAppSettings { + requestContactsCallPermissionLauncher.launch(permissionsToRequest) + } + } + + }) + } + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + + navController = Navigation.findNavController(view) + initView() + } + + private fun initView() { + + val layoutParams = binding.closeBtn.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.closeBtn.layoutParams = layoutParams + + binding.recycleView.adapter = addFriendRVA + binding.recycleView.setHasFixedSize(true) + + binding.closeBtn.setOnClickListener { + (activity as AddFriendActivity).finish() + } + + binding.searchOpenEditT.setOnEditorActionListener { _, i, _ -> + if (i == EditorInfo.IME_ACTION_DONE) { + if (!binding.searchOpenEditT.text.isNullOrEmpty()) { + viewModel.searchTag() + } + true + } + false + } + binding.searchOpenEditT.addTextChangedListener { + viewModel.resetList() + } + + binding.addCV.setOnClickListener { + requestContactsCallPermissionLauncher.launch(permissionsToRequest) + } + + binding.searchOpenBtnT.setOnClickListener { + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0) + viewModel.searchTag() + } + } + + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.addEvent.collect { event -> + when (event) { + is AddFriendViewModel.AddEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + is AddFriendViewModel.AddEvent.NotificationChange -> { + addFriendRVA.notifyDataSetChanged() + } + + else -> {} + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.addFriendListUI.collect { friends -> + addFriendRVA.submitList(friends) + } + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/SearchFriendNumberFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/SearchFriendNumberFragment.kt new file mode 100644 index 000000000..20112b44b --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/SearchFriendNumberFragment.kt @@ -0,0 +1,260 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addfriend + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentFriendSearchNumberBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.addfriend.adapter.AddFriendRVA +import com.droidblossom.archive.util.ContactsUtils +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import okhttp3.internal.notify + + +@AndroidEntryPoint +class SearchFriendNumberFragment : + BaseFragment(R.layout.fragment_friend_search_number) { + + override val viewModel: AddFriendViewModelImpl by viewModels() + + lateinit var navController: NavController + + private val addFriendRVA by lazy { + AddFriendRVA { position -> + viewModel.checkAddFriendList(position) + } + } + + + private val permissionsToRequest: Array by lazy { + val permissionsList = mutableListOf() + permissionsList.add(Manifest.permission.READ_CONTACTS) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + permissionsList.add(Manifest.permission.READ_PHONE_NUMBERS) + } + + permissionsList.toTypedArray() + } + + private val requestContactsCallPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + viewModel.contactsSearch(ContactsUtils.getContacts(requireContext())) + } + permissions.none { it.value } -> { + handleAllPermissionsDenied() + } + else -> { + handlePartialPermissionsDenied(permissions) + } + } + } + + private fun handleAllPermissionsDenied() { + if (permissionsToRequest.size > 1) { + if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS) || + shouldShowRequestPermissionRationale(Manifest.permission.READ_PHONE_NUMBERS) + ) { + showToastMessage("앱에서 친구를 찾기 위해 연락처, 전화 접근 권한이 필요합니다.") + } else { + showSettingsDialog(PermissionDialogFragment.PermissionType.CONTACTS_AND_CALL, object : PermissionDialogButtonClickListener{ + override fun onLeftButtonClicked() { + showToastMessage("앱에서 친구를 찾기 위해 연락처, 전화 접근 권한이 필요합니다.") + requireActivity().finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings{requestContactsCallPermissionLauncher.launch(permissionsToRequest)} + } + }) + } + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { + showToastMessage("앱에서 친구를 찾기 위해 연락처 접근 권한이 필요합니다.") + } else { + showSettingsDialog(PermissionDialogFragment.PermissionType.CONTACTS, object : PermissionDialogButtonClickListener{ + override fun onLeftButtonClicked() { + showToastMessage("앱에서 친구를 찾기 위해 연락처 접근 권한이 필요합니다.") + requireActivity().finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings{requestContactsCallPermissionLauncher.launch(permissionsToRequest)} + } + + }) + } + } + } + + private fun handlePartialPermissionsDenied(permissions: Map) { + permissions.forEach { (perm, granted) -> + if (!granted) { + when (perm) { + Manifest.permission.READ_CONTACTS -> + showPermissionDialog( PermissionDialogFragment.PermissionType.CONTACTS ) + + Manifest.permission.READ_PHONE_NUMBERS -> + showPermissionDialog( PermissionDialogFragment.PermissionType.CALL ) + } + } + } + } + + private fun showPermissionDialog(permissionType: PermissionDialogFragment.PermissionType) { + if (shouldShowRequestPermissionRationale(permissionType.toString())) { + showToastMessage("앱에서 친구를 찾기 위해 ${permissionType.description} 권한이 필요합니다.") + } else { + showSettingsDialog(permissionType, object : PermissionDialogButtonClickListener{ + override fun onLeftButtonClicked() { + showToastMessage("앱에서 친구를 찾기 위해 ${permissionType.description} 권한이 필요합니다.") + requireActivity().finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings{requestContactsCallPermissionLauncher.launch(permissionsToRequest)} + } + + }) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + + navController = Navigation.findNavController(view) + initView() + + requestContactsCallPermissionLauncher.launch(permissionsToRequest) + } + + @SuppressLint("ClickableViewAccessibility", "NotifyDataSetChanged") + private fun initView() { + + val layoutParams = binding.closeBtn.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.closeBtn.layoutParams = layoutParams + + binding.recycleView.adapter = addFriendRVA + binding.recycleView.setHasFixedSize(true) + + binding.closeBtn.setOnClickListener { + navController.popBackStack() + } + + binding.searchOpenEditT.setOnEditorActionListener { _, i, _ -> + if (i == EditorInfo.IME_ACTION_DONE) { + if (!binding.searchOpenEditT.text.isNullOrEmpty()) { + viewModel.searchTag() + } + true + } + false + } + binding.searchOpenEditT.addTextChangedListener { + viewModel.resetList() + } + + binding.searchOpenBtnT.setOnClickListener { + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0) + viewModel.searchTag() + } + + binding.searchOpenEditT.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + viewModel.closeSearchNum() + } + } + binding.root.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val focusedView = binding.searchOpenBtn + val outRect = Rect() + focusedView.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + focusedView.clearFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(focusedView.windowToken, 0) + } + } + false + } + + binding.recycleView.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val focusedView = binding.searchOpenBtn + val outRect = Rect() + focusedView.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(focusedView.windowToken, 0) + } + } + false + } + } + + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.addEvent.collect { event -> + when (event) { + is AddFriendViewModel.AddEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + is AddFriendViewModel.AddEvent.CloseLoading -> { + dismissLoading() + } + + is AddFriendViewModel.AddEvent.OpenLoading -> { + showLoading(requireContext()) + } + + is AddFriendViewModel.AddEvent.NotificationChange -> { + addFriendRVA.notifyDataSetChanged() + } + + else -> {} + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.addFriendListUI.collect { friends -> + addFriendRVA.submitList(friends) + } + } + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/adapter/AddFriendRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/adapter/AddFriendRVA.kt new file mode 100644 index 000000000..c27b0226c --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addfriend/adapter/AddFriendRVA.kt @@ -0,0 +1,59 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addfriend.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemAddFriendBinding +import com.droidblossom.archive.domain.model.friend.FriendsSearchResponse + +class AddFriendRVA(private val check: (Int) -> Unit) : + ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemAddFriendBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(data: FriendsSearchResponse) { + binding.item = data + binding.root.setOnClickListener { + check(bindingAdapterPosition) + notifyItemChanged(bindingAdapterPosition) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder( + ItemAddFriendBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: FriendsSearchResponse, + newItem: FriendsSearchResponse + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: FriendsSearchResponse, + newItem: FriendsSearchResponse + ): Boolean { + return oldItem == newItem + } + + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupActivity.kt new file mode 100644 index 000000000..b8655e277 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupActivity.kt @@ -0,0 +1,53 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addgroup + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.viewModels +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivityAddGroupBinding +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.ui.mypage.friend.addgroup.adapter.AddGroupVPA + +class AddGroupActivity : + BaseActivity(R.layout.activity_add_group) { + override val viewModel: AddGroupViewModelImpl by viewModels() + + private val addGroupVPA by lazy { + AddGroupVPA(this) + } + + override fun observeData() { + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding.vm = viewModel + initView() + } + + private fun initView() { + val layoutParams = binding.appBar.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.appBar.layoutParams = layoutParams + + val layoutParamsToolBar = binding.toolBar.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.toolBar.layoutParams = layoutParamsToolBar + + binding.vp.adapter = addGroupVPA + binding.vp. currentItem = 0 + binding.closeBtn.setOnClickListener { + finish() + } + } + + companion object { + const val ADD_GROUP = "add_group" + + fun newIntent(context: Context) = + Intent(context, AddGroupActivity::class.java) + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupFragment.kt new file mode 100644 index 000000000..00c3650d1 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupFragment.kt @@ -0,0 +1,28 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addgroup + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentAddGroupBinding +import com.droidblossom.archive.presentation.base.BaseFragment + +class AddGroupFragment : + BaseFragment(R.layout.fragment_add_group) { + + override val viewModel: AddGroupViewModelImpl by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initView() + } + + private fun initView() { + + } + + override fun observeData() { + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupViewModel.kt new file mode 100644 index 000000000..c2caaeb44 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupViewModel.kt @@ -0,0 +1,4 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addgroup + +interface AddGroupViewModel { +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupViewModelImpl.kt new file mode 100644 index 000000000..a3ece0fc4 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/AddGroupViewModelImpl.kt @@ -0,0 +1,10 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addgroup + +import com.droidblossom.archive.presentation.base.BaseViewModel +import javax.inject.Inject + +class AddGroupViewModelImpl @Inject constructor( + +) : BaseViewModel(), AddGroupViewModel { + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/adapter/AddGroupVPA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/adapter/AddGroupVPA.kt new file mode 100644 index 000000000..a29a020a3 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/addgroup/adapter/AddGroupVPA.kt @@ -0,0 +1,19 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.addgroup.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.droidblossom.archive.presentation.ui.mypage.friend.addgroup.AddGroupFragment + +class AddGroupVPA(activity: FragmentActivity) : FragmentStateAdapter(activity){ + + private val fragmentList = listOf( + AddGroupFragment() + ) + + override fun getItemCount(): Int = fragmentList.size + + override fun createFragment(position: Int): Fragment { + return fragmentList[position] + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/page/FriendListFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/page/FriendListFragment.kt new file mode 100644 index 000000000..0c8c5a4df --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/page/FriendListFragment.kt @@ -0,0 +1,84 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.page + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentListFriendBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModel +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModelImpl +import com.droidblossom.archive.presentation.ui.mypage.friend.adapter.FriendRVA +import kotlinx.coroutines.launch + +class FriendListFragment : + BaseFragment(R.layout.fragment_list_friend) { + + override val viewModel: FriendViewModelImpl by activityViewModels() + + private val friendRVA by lazy { + FriendRVA(requireContext(), + { prev, curr -> + viewModel.changeDeleteOpen(prev, curr) + }, { friend -> + viewModel.deleteFriend(friend) + } + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initView() + } + + private fun initView() { + binding.friendRV.adapter = friendRVA + binding.friendRV.setHasFixedSize(true) + binding.friendRV.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + + if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_DRAGGING) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + + if (totalItemCount - lastVisibleItemPosition <= 3) { + viewModel.onScrollNearBottomFriend() + } + } + } + }) + } + + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.friendListUI.collect { friends -> + friendRVA.submitList(friends) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.friendEvent.collect { event -> + when (event) { + is FriendViewModel.FriendEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/page/GroupListFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/page/GroupListFragment.kt new file mode 100644 index 000000000..b710b62bb --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friend/page/GroupListFragment.kt @@ -0,0 +1,54 @@ +package com.droidblossom.archive.presentation.ui.mypage.friend.page + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentListFriendBinding +import com.droidblossom.archive.databinding.FragmentListGroupBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModel +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModelImpl +import com.droidblossom.archive.presentation.ui.mypage.friend.adapter.FriendRVA +import kotlinx.coroutines.launch + +class GroupListFragment : + BaseFragment(R.layout.fragment_list_group) { + + override val viewModel: FriendViewModelImpl by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initView() + } + + private fun initView() { + + } + + override fun observeData() { + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.friendEvent.collect { event -> + when (event) { + is FriendViewModel.FriendEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptActivity.kt new file mode 100644 index 000000000..dca576317 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptActivity.kt @@ -0,0 +1,85 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.viewModels +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivityFriendAcceptBinding +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendActivity +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.adapter.FriendAcceptVPA +import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class FriendAcceptActivity : + BaseActivity(R.layout.activity_friend_accept) { + override val viewModel: FriendAcceptViewModelImpl by viewModels() + + private val friendAcceptVPA by lazy { + FriendAcceptVPA(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initView() + + viewModel.getFriendAcceptList() + } + + private fun initView() { + val layoutParams = binding.closeBtn.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.closeBtn.layoutParams = layoutParams + + binding.vp.adapter = friendAcceptVPA + + binding.closeBtn.setOnClickListener { + finish() + } + + TabLayoutMediator(binding.tab, binding.vp) { tab, position -> + tab.text = when (position) { + 0 -> getString(R.string.groupAccept) + 1 -> getString(R.string.friendAccept) + else -> null + } + }.attach() + + + intent.getStringExtra(FRIENDACCEPT)?.let { type -> + when (type) { + GROUP -> { + binding.tab.getTabAt(0)?.select() + } + + FRIEND -> { + binding.tab.getTabAt(1)?.select() + } + + else -> { + binding.tab.getTabAt(0)?.select() + } + + } + } + } + + override fun observeData() { + + } + + companion object { + const val FRIEND = "friend" + const val GROUP = "group" + const val FRIENDACCEPT = "friend_accept" + + fun newIntent(context: Context, type: String) = + Intent(context, FriendAcceptActivity::class.java).apply { + putExtra(FRIENDACCEPT, type) + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptViewModel.kt new file mode 100644 index 000000000..0823589a2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptViewModel.kt @@ -0,0 +1,22 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept + +import com.droidblossom.archive.domain.model.friend.Friend +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModel +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface FriendAcceptViewModel { + + val friendAcceptEvent: SharedFlow + val friendAcceptList: StateFlow> + + fun getFriendAcceptList() + fun onScrollNearBottom() + fun denyRequest(friend: Friend) + fun acceptRequest(friend: Friend) + + + sealed class FriendAcceptEvent { + data class ShowToastMessage(val message: String) : FriendAcceptEvent() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptViewModelImpl.kt new file mode 100644 index 000000000..a42701b80 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/FriendAcceptViewModelImpl.kt @@ -0,0 +1,139 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept + +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.friend.Friend +import com.droidblossom.archive.domain.model.friend.FriendAcceptRequest +import com.droidblossom.archive.domain.usecase.friend.FriendDenyRequestUseCase +import com.droidblossom.archive.domain.usecase.friend.FriendsAcceptRequestUseCase +import com.droidblossom.archive.domain.usecase.friend.FriendsRequestsPageUseCase +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModel +import com.droidblossom.archive.util.DateUtils +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class FriendAcceptViewModelImpl @Inject constructor( + private val friendsRequestsPageUseCase: FriendsRequestsPageUseCase, + private val denyRequestUseCase: FriendDenyRequestUseCase, + private val acceptRequestUseCase: FriendsAcceptRequestUseCase +) : BaseViewModel(), FriendAcceptViewModel { + + private val _friendAcceptEvent = MutableSharedFlow() + override val friendAcceptEvent: SharedFlow + get() = _friendAcceptEvent.asSharedFlow() + + private val _friendAcceptList = MutableStateFlow>(listOf()) + override val friendAcceptList: StateFlow> + get() = _friendAcceptList + + private val friendHasNextPage = MutableStateFlow(true) + private val friendLastCreatedTime = MutableStateFlow(DateUtils.dataServerString) + + private val scrollEventChannel = Channel(Channel.CONFLATED) + private val scrollEventFlow = + scrollEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) + + private var getAcceptRequestListJob: Job? = null + + init { + viewModelScope.launch { + scrollEventFlow.collect { + getFriendAcceptList() + } + } + } + + override fun onScrollNearBottom() { + scrollEventChannel.trySend(Unit) + } + + override fun getFriendAcceptList() { + getAcceptRequestListJob?.cancel() + getAcceptRequestListJob = viewModelScope.launch { + if (friendHasNextPage.value) { + friendsRequestsPageUseCase( + PagingRequestDto( + 15, + friendLastCreatedTime.value + ) + ).collect { result -> + result.onSuccess { + friendHasNextPage.value = it.hasNext + _friendAcceptList.emit(friendAcceptList.value + it.friends) + if (friendAcceptList.value.isNotEmpty()) { + friendLastCreatedTime.value = it.friends.last().createdAt + } + + }.onFail { + _friendAcceptEvent.emit( + FriendViewModel.FriendEvent.ShowToastMessage( + "친구 리스트 불러오기 실패. 잠시후 시도해 주세요" + ) + ) + } + } + } + } + } + + override fun denyRequest(friend: Friend) { + viewModelScope.launch { + denyRequestUseCase(friend.id).collect { result -> + result.onSuccess { + removeItem(friend) + }.onFail { + _friendAcceptEvent.emit( + FriendViewModel.FriendEvent.ShowToastMessage( + "요청 실패. 잠시후 시도해 주세요" + ) + ) + } + + } + } + } + + override fun acceptRequest(friend: Friend) { + viewModelScope.launch { + acceptRequestUseCase(FriendAcceptRequest(friend.id)).collect { result -> + result.onSuccess { + removeItem(friend) + }.onFail { + _friendAcceptEvent.emit( + FriendViewModel.FriendEvent.ShowToastMessage( + "요청 실패. 잠시후 시도해 주세요" + ) + ) + } + + } + } + } + + fun removeItem(friend: Friend) { + viewModelScope.launch { + val newList = _friendAcceptList.value.toMutableList() + newList.remove(friend) + _friendAcceptList.emit(newList) + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/adapter/FriendAcceptRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/adapter/FriendAcceptRVA.kt new file mode 100644 index 000000000..c22247a74 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/adapter/FriendAcceptRVA.kt @@ -0,0 +1,65 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemAcceptBinding +import com.droidblossom.archive.domain.model.friend.Friend + + +class FriendAcceptRVA( + private val onDeny: (Friend) -> Unit, + private val onAccept: (Friend) -> Unit +) : + ListAdapter(differ) { + + + inner class ItemViewHolder( + private val binding: ItemAcceptBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(data: Friend) { + binding.item = data + binding.acceptBtn.setOnClickListener { + onAccept(data) + } + binding.denyBtn.setOnClickListener { + onDeny(data) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder( + ItemAcceptBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Friend, + newItem: Friend + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: Friend, + newItem: Friend + ): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/adapter/FriendAcceptVPA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/adapter/FriendAcceptVPA.kt new file mode 100644 index 000000000..7d2eb305c --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/adapter/FriendAcceptVPA.kt @@ -0,0 +1,21 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.page.FriendAcceptFragment +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.page.GroupAcceptFragment + +class FriendAcceptVPA(activity: FragmentActivity) : FragmentStateAdapter(activity) { + + private val fragmentList = listOf( + GroupAcceptFragment(), + FriendAcceptFragment() + ) + + override fun getItemCount(): Int = fragmentList.size + + override fun createFragment(position: Int): Fragment { + return fragmentList[position] + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/page/FriendAcceptFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/page/FriendAcceptFragment.kt new file mode 100644 index 000000000..81bc2849a --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/page/FriendAcceptFragment.kt @@ -0,0 +1,86 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept.page + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentAcceptBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModel +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.FriendAcceptViewModelImpl +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.adapter.FriendAcceptRVA +import kotlinx.coroutines.launch + +class FriendAcceptFragment : + BaseFragment(R.layout.fragment_accept) { + + override val viewModel: FriendAcceptViewModelImpl by activityViewModels() + + private val friendAcceptRVA by lazy { + FriendAcceptRVA( + { denyFriend -> + viewModel.denyRequest(denyFriend) + }, + { acceptFriend -> + viewModel.acceptRequest(acceptFriend) + } + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initRV() + } + + private fun initRV() { + binding.rv.adapter = friendAcceptRVA + binding.rv.setHasFixedSize(true) + binding.rv.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + + if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_DRAGGING) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + + if (totalItemCount - lastVisibleItemPosition <= 5) { + viewModel.onScrollNearBottom() + } + } + } + }) + } + + override fun observeData() { + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.friendAcceptEvent.collect { event -> + when (event) { + is FriendViewModel.FriendEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.friendAcceptList.collect { friendAccepts -> + friendAcceptRVA.submitList(friendAccepts) + } + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/page/GroupAcceptFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/page/GroupAcceptFragment.kt new file mode 100644 index 000000000..4c0e5c00d --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/friendaccept/page/GroupAcceptFragment.kt @@ -0,0 +1,47 @@ +package com.droidblossom.archive.presentation.ui.mypage.friendaccept.page + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentAcceptBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.friend.FriendViewModel +import com.droidblossom.archive.presentation.ui.mypage.friendaccept.FriendAcceptViewModelImpl +import kotlinx.coroutines.launch + +class GroupAcceptFragment : + BaseFragment(R.layout.fragment_accept) { + + override val viewModel: FriendAcceptViewModelImpl by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initView() + } + + private fun initView() { + + } + + override fun observeData() { + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.friendAcceptEvent.collect { event -> + when (event) { + is FriendViewModel.FriendEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingActivity.kt new file mode 100644 index 000000000..64d6b3ea7 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingActivity.kt @@ -0,0 +1,43 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.viewModels +import androidx.databinding.DataBindingUtil.setContentView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivitySettingBinding +import com.droidblossom.archive.databinding.FragmentMyPageBinding +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleActivity +import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleActivity.Companion.CREATE_CAPSULE +import com.droidblossom.archive.presentation.ui.mypage.MyPageViewModelImpl +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingActivity : + BaseActivity(R.layout.activity_setting) { + override val viewModel: SettingViewModelImpl by viewModels() + override fun observeData() {} + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initView() + } + + private fun initView() { + val layoutParams = binding.settingFragmentContainer.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.settingFragmentContainer.layoutParams = layoutParams + } + + companion object { + const val SETTING = "setting" + + fun newIntent(context: Context) = + Intent(context, SettingActivity::class.java) + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingMainFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingMainFragment.kt new file mode 100644 index 000000000..f3abeffd0 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingMainFragment.kt @@ -0,0 +1,124 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSettingMainBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.customview.CommonDialogFragment +import com.droidblossom.archive.presentation.ui.auth.AuthActivity +import com.droidblossom.archive.util.DataStoreUtils +import com.google.android.gms.oss.licenses.OssLicensesMenuActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class SettingMainFragment : + BaseFragment(R.layout.fragment_setting_main) { + + override val viewModel: SettingViewModelImpl by viewModels() + + @Inject + lateinit var dataStoreUtils: DataStoreUtils + lateinit var navController: NavController + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + navController = Navigation.findNavController(view) + } + + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.settingMainEvents.collect { event -> + when (event) { + SettingViewModel.SettingMainEvent.Back -> { + (activity as SettingActivity).finish() + } + + SettingViewModel.SettingMainEvent.GoAgree -> { + navController.navigate(R.id.action_settingMainFragment_to_settingAgreeFragment) + } + + SettingViewModel.SettingMainEvent.GoInquire -> { + goEmailIntent() + } + + SettingViewModel.SettingMainEvent.GoLicenses -> { + startActivity( + Intent( + requireContext(), + OssLicensesMenuActivity::class.java + ) + ) + OssLicensesMenuActivity.setActivityTitle("오픈소스 라이선스") + } + + SettingViewModel.SettingMainEvent.GoLogout -> { + val sheet = CommonDialogFragment.newIntent("정말 로그아웃 하시겠습니까?", "로그아웃") { + CoroutineScope(Dispatchers.IO).launch { + dataStoreUtils.resetTokenData() + } + AuthActivity.goAuth(requireContext()) + } + sheet.show(parentFragmentManager, "logoutDialog") + } + + SettingViewModel.SettingMainEvent.GoNotice -> { + navController.navigate(R.id.action_settingMainFragment_to_settingNoticeFragment) + } + + SettingViewModel.SettingMainEvent.GoNotification -> { + navController.navigate(R.id.action_settingMainFragment_to_settingNotificationFragment) + } + + SettingViewModel.SettingMainEvent.GoUser -> { + navController.navigate(R.id.action_settingMainFragment_to_settingUserFragment) + } + + is SettingViewModel.SettingMainEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + } + + private fun goEmailIntent() { + val emailSelectorIntent = Intent(Intent.ACTION_SENDTO) + emailSelectorIntent.data = Uri.parse("mailto:") + + val address = arrayOf("ARchive@address.com") + + val emailIntent = Intent(Intent.ACTION_SEND) + emailIntent.putExtra(Intent.EXTRA_EMAIL, address) + emailIntent.putExtra(Intent.EXTRA_SUBJECT, "") + emailIntent.putExtra(Intent.EXTRA_TEXT, "") + + emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + emailIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + emailIntent.selector = emailSelectorIntent + + startActivity(emailIntent) +// if (emailIntent.resolveActivity((activity as SettingActivity).packageManager) != null) { +// startActivity(emailIntent) +// } else { +// showToastMessage("메일을 연결할 앱이 없습니다.") +// } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingViewModel.kt new file mode 100644 index 000000000..5682061df --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingViewModel.kt @@ -0,0 +1,39 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting + +import com.droidblossom.archive.presentation.ui.skin.skinmake.SkinMakeViewModel +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface SettingViewModel { + + val settingMainEvents: SharedFlow + + val notificationEnable : StateFlow + val settingNotificationEvents : SharedFlow + + fun back() + fun goUser() + fun goNotification() + fun goNotice() + fun goAgree() + fun goInquire() + fun goLicenses() + fun goLogout() + + sealed class SettingMainEvent { + object Back : SettingMainEvent() + object GoUser : SettingMainEvent() + object GoNotification : SettingMainEvent() + object GoNotice : SettingMainEvent() + object GoAgree : SettingMainEvent() + object GoInquire : SettingMainEvent() + object GoLicenses : SettingMainEvent() + object GoLogout : SettingMainEvent() + data class ShowToastMessage(val message : String) : SettingMainEvent() + } + + sealed class SettingNotificationEvent { + object Back : SettingNotificationEvent() + data class ShowToastMessage(val message : String) : SettingNotificationEvent() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingViewModelImpl.kt new file mode 100644 index 000000000..cb88a7931 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/SettingViewModelImpl.kt @@ -0,0 +1,119 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting + +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.domain.usecase.member.NotificationEnabledUseCase +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.presentation.ui.mypage.MyPageViewModel +import com.droidblossom.archive.presentation.ui.skin.skinmake.SkinMakeViewModel +import com.droidblossom.archive.util.DataStoreUtils +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingViewModelImpl @Inject constructor( + private val notificationEnabledUseCase: NotificationEnabledUseCase, + private val dataStoreUtils: DataStoreUtils +) : BaseViewModel(), SettingViewModel { + + //main + private val _settingMainEvents = MutableSharedFlow() + override val settingMainEvents: SharedFlow + get() = _settingMainEvents.asSharedFlow() + + //notification + private val _notificationEnable = MutableStateFlow(false) + override val notificationEnable: StateFlow + get() = _notificationEnable + + private val _settingNotificationEvent = MutableSharedFlow() + override val settingNotificationEvents: SharedFlow + get() = _settingNotificationEvent.asSharedFlow() + + + override fun back() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.Back) + } + } + + override fun goUser() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoUser) + } + } + + override fun goNotification() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoNotification) + } + } + + override fun goNotice() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoNotice) + } + } + + override fun goAgree() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoAgree) + } + } + + override fun goInquire() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoInquire) + } + } + + override fun goLicenses() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoLicenses) + } + } + + override fun goLogout() { + viewModelScope.launch { + _settingMainEvents.emit(SettingViewModel.SettingMainEvent.GoLogout) + } + } + + //Notification Setting + fun getNotificationEnable() { + viewModelScope.launch { + _notificationEnable.emit(dataStoreUtils.fetchNotificationsEnabled()) + } + } + + fun postNotificationEnable(enabled: Boolean) { + viewModelScope.launch { + if (enabled xor notificationEnable.value) { + dataStoreUtils.saveNotificationsEnabled(enabled) + notificationEnabledUseCase(enabled).collect { result -> + result.onSuccess { + Log.d("알림변경", "성공") + _settingNotificationEvent.emit(SettingViewModel.SettingNotificationEvent.ShowToastMessage("알림 설정을 변경했습니다.")) + }.onFail { + Log.d("알림변경", "실패") + _settingNotificationEvent.emit(SettingViewModel.SettingNotificationEvent.ShowToastMessage("알림을 변경 실패.")) + + } + } + _settingNotificationEvent.emit(SettingViewModel.SettingNotificationEvent.Back) + } else { + _settingNotificationEvent.emit(SettingViewModel.SettingNotificationEvent.Back) + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/AgreeRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/AgreeRVA.kt new file mode 100644 index 000000000..ec4b3ccdf --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/AgreeRVA.kt @@ -0,0 +1,68 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.adapter + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ItemAgreeBinding +import com.droidblossom.archive.domain.model.setting.Agree + + +class AgreeRVA : + ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemAgreeBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: Agree) { + binding.item = data + binding.moreBtn.setOnClickListener { + if (data.isOpen) { + data.isOpen = false + binding.contentSV.isGone = true + binding.moreBtn.setImageResource(R.drawable.ic_plus_24) + } else { + data.isOpen = true + binding.contentSV.isVisible = true + binding.moreBtn.setImageResource(R.drawable.ic_minus_24) + } + } + } + + } + + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemAgreeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Agree, newItem: Agree): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Agree, newItem: Agree): Boolean { + return oldItem == newItem + } + } + } +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/NoticeContentRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/NoticeContentRVA.kt new file mode 100644 index 000000000..28cdd7282 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/NoticeContentRVA.kt @@ -0,0 +1,55 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.adapter + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemNoticeBinding +import com.droidblossom.archive.databinding.ItemNoticeContentBinding +import com.droidblossom.archive.domain.model.setting.Notice +import com.droidblossom.archive.domain.model.setting.NoticeContent + + +class NoticeContentRVA : + ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemNoticeContentBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: NoticeContent) { + binding.item = data + } + } + + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemNoticeContentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: NoticeContent, newItem: NoticeContent): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: NoticeContent, newItem: NoticeContent): Boolean { + return oldItem == newItem + } + } + } +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/NoticeRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/NoticeRVA.kt new file mode 100644 index 000000000..a7cad41f2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/adapter/NoticeRVA.kt @@ -0,0 +1,72 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ItemNoticeBinding +import com.droidblossom.archive.domain.model.setting.Notice + + +class NoticeRVA : + ListAdapter(differ) { + + inner class ItemViewHolder( + private val binding: ItemNoticeBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: Notice) { + binding.item = data + val adapter = NoticeContentRVA() + binding.contentRVA.adapter = adapter + adapter.submitList(data.contents) + binding.noticeLayout.setOnClickListener{ + if (data.isOpen) { + data.isOpen = false + binding.contentLayout.isGone = true + binding.moreBtn.rotation = 0f + binding.line.setBackgroundResource(R.color.gray_400) + } else { + data.isOpen = true + binding.contentLayout.isVisible = true + binding.moreBtn.rotation = 180f + binding.line.setBackgroundResource(R.color.black) + } + } + } + + } + + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemNoticeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Notice, newItem: Notice): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Notice, newItem: Notice): Boolean { + return oldItem == newItem + } + } + } +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingAgreeFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingAgreeFragment.kt new file mode 100644 index 000000000..239c080b0 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingAgreeFragment.kt @@ -0,0 +1,52 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.page + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSettingAgreeBinding +import com.droidblossom.archive.domain.model.setting.Agree +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.setting.SettingViewModelImpl +import com.droidblossom.archive.presentation.ui.mypage.setting.adapter.AgreeRVA +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingAgreeFragment : + BaseFragment(R.layout.fragment_setting_agree) { + + override val viewModel: SettingViewModelImpl by viewModels() + lateinit var navController: NavController + + private val agreeAdapter by lazy { + AgreeRVA() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + navController = Navigation.findNavController(view) + initRVA() + } + + private fun initRVA() { + binding.adapter.adapter = agreeAdapter + binding.adapter.setHasFixedSize(true) + agreeAdapter.submitList( + listOf( + Agree("이용약관 1장", getString(R.string.agree_content)), + Agree("이용약관 2장", getString(R.string.agree_content)), + Agree("사용자 정보 정책", getString(R.string.agree_content)), + ) + ) + binding.backBtn.setOnClickListener { + navController.popBackStack() + } + } + + override fun observeData() { + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingNoticeFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingNoticeFragment.kt new file mode 100644 index 000000000..3609cda20 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingNoticeFragment.kt @@ -0,0 +1,67 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.page + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSettingNoticeBinding +import com.droidblossom.archive.domain.model.setting.Notice +import com.droidblossom.archive.domain.model.setting.NoticeContent +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.setting.SettingViewModelImpl +import com.droidblossom.archive.presentation.ui.mypage.setting.adapter.NoticeRVA +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingNoticeFragment : + BaseFragment(R.layout.fragment_setting_notice) { + + override val viewModel: SettingViewModelImpl by viewModels() + lateinit var navController: NavController + + private val noticeAdapter by lazy { + NoticeRVA() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + navController = Navigation.findNavController(view) + initView() + } + + private fun initView() { + binding.adapter.adapter = noticeAdapter + binding.adapter.setHasFixedSize(true) + noticeAdapter.submitList( + listOf( + Notice( + "아카이브 업데이트 : 1.1.0", + "2024/03/01", + listOf( + NoticeContent("업데이트"), + NoticeContent("했습니다 : 1.1.0") + ) + ), + Notice( + "아카이브 업데이트 : 1.0.9", + "2024/03/01", + listOf( + NoticeContent("업데이트"), + NoticeContent("hey"), + NoticeContent("keep going"), + ) + ) + ) + ) + binding.backBtn.setOnClickListener { + navController.popBackStack() + } + } + + override fun observeData() { + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingNotificationFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingNotificationFragment.kt new file mode 100644 index 000000000..b3ebd38db --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingNotificationFragment.kt @@ -0,0 +1,78 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.page + +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSettingNotificationBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.setting.SettingViewModel +import com.droidblossom.archive.presentation.ui.mypage.setting.SettingViewModelImpl +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SettingNotificationFragment : + BaseFragment(R.layout.fragment_setting_notification) { + + override val viewModel: SettingViewModelImpl by viewModels() + lateinit var navController: NavController + private lateinit var callback: OnBackPressedCallback + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + navController = Navigation.findNavController(view) + initView() + } + + private fun initView() { + viewModel.getNotificationEnable() + + binding.backBtn.setOnClickListener { + viewModel.postNotificationEnable(binding.notificationSwitch.isChecked) + } + + binding.notificationSwitch.setOnCheckedChangeListener { _, b -> + callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + viewModel.postNotificationEnable(b) + } + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) + } + } + + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.notificationEnable.collect { enable -> + binding.notificationSwitch.isChecked = enable + } + } + } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.settingNotificationEvents.collect { event -> + when (event) { + is SettingViewModel.SettingNotificationEvent.Back -> { + navController.popBackStack() + } + + is SettingViewModel.SettingNotificationEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + else -> {} + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingUserFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingUserFragment.kt new file mode 100644 index 000000000..fab8ceb21 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/mypage/setting/page/SettingUserFragment.kt @@ -0,0 +1,38 @@ +package com.droidblossom.archive.presentation.ui.mypage.setting.page + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSettingNoticeBinding +import com.droidblossom.archive.databinding.FragmentSettingUserBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.mypage.setting.SettingViewModelImpl +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingUserFragment : + BaseFragment(R.layout.fragment_setting_user) { + + override val viewModel: SettingViewModelImpl by viewModels() + lateinit var navController: NavController + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + navController = Navigation.findNavController(view) + initView() + } + + private fun initView() { + binding.backBtn.setOnClickListener { + navController.popBackStack() + } + } + + override fun observeData() { + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinFragment.kt index 39e7e4175..d5993031d 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinFragment.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContentProviderCompat.requireContext import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -25,6 +26,7 @@ import com.droidblossom.archive.presentation.ui.home.HomeFragment import com.droidblossom.archive.presentation.ui.skin.adapter.MySkinRVA import com.droidblossom.archive.presentation.ui.skin.skinmake.SkinMakeActivity import com.droidblossom.archive.util.LocationUtil +import com.droidblossom.archive.util.updateTopConstraintsForSearch import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -39,7 +41,6 @@ class SkinFragment : BaseFragment(R.layo override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.vm = viewModel - viewModel.getSkinList() initRVA() initSearchEdit() binding.createCapsuleLayout.setOnClickListener { @@ -51,19 +52,22 @@ class SkinFragment : BaseFragment(R.layo binding.viewHeaderTitle.layoutParams = layoutParams } + private fun initRVA() { binding.skinRV.adapter = mySkinRVA + binding.skinRV.setHasFixedSize(true) binding.skinRV.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) - val lastVisibleItemPosition = - (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition() - val totalItemViewCount = recyclerView.adapter!!.itemCount - 1 - - if (newState == 2 && !recyclerView.canScrollVertically(1) - && lastVisibleItemPosition == totalItemViewCount - ) { - viewModel.getSkinList() + + if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_DRAGGING) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + + if (totalItemCount - lastVisibleItemPosition <= 5) { + viewModel.onScrollNearBottom() + } } } }) @@ -106,6 +110,14 @@ class SkinFragment : BaseFragment(R.layo viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.isSearchOpen.collect { + val layoutParams = binding.skinRV.layoutParams as ConstraintLayout.LayoutParams + layoutParams.updateTopConstraintsForSearch( + isSearchOpen = it, + searchOpenView = binding.searchOpenBtn, + searchView = binding.searchBtn, + additionalMarginDp = 16f, + resources = resources + ) if (it){ binding.searchOpenEditT.requestFocus() val imm = requireActivity().getSystemService(InputMethodManager::class.java) @@ -119,9 +131,7 @@ class SkinFragment : BaseFragment(R.layo override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) - if (hidden) { - - } else { + if (!hidden) { viewModel.clearSkins() viewModel.closeSearchSkin() } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModel.kt index 86a6e891d..c291e275f 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModel.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModel.kt @@ -23,6 +23,7 @@ interface SkinViewModel { fun updateMySkinsUI() fun clearSkins() + fun onScrollNearBottom() sealed class SkinEvent { object ToSkinMake : SkinEvent() diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModelImpl.kt index 6d41ed2c2..787f27e3c 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/SkinViewModelImpl.kt @@ -1,7 +1,7 @@ package com.droidblossom.archive.presentation.ui.skin import androidx.lifecycle.viewModelScope -import com.droidblossom.archive.data.dto.capsule_skin.request.CapsuleSkinsPageRequestDto +import com.droidblossom.archive.data.dto.common.PagingRequestDto import com.droidblossom.archive.domain.model.common.CapsuleSkinSummary import com.droidblossom.archive.domain.usecase.capsule_skin.CapsuleSkinsPageUseCase import com.droidblossom.archive.presentation.base.BaseViewModel @@ -9,12 +9,16 @@ import com.droidblossom.archive.util.DateUtils import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel @@ -48,20 +52,40 @@ class SkinViewModelImpl @Inject constructor( override val isSearchOpen: StateFlow get() = _isSearchOpen + private val scrollEventChannel = Channel(Channel.CONFLATED) + private val scrollEventFlow = scrollEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) - override fun getSkinList() { + private var getSkinLstJob: Job? = null + + init { viewModelScope.launch { - if (hasNextSkins.value) { + scrollEventFlow.collect{ + getSkinList() + } + } + } + + override fun onScrollNearBottom() { + scrollEventChannel.trySend(Unit) + } + + + override fun getSkinList() { + if (hasNextSkins.value){ + getSkinLstJob?.cancel() + getSkinLstJob = viewModelScope.launch { capsuleSkinsPageUseCase( - CapsuleSkinsPageRequestDto( + PagingRequestDto( 15, - _lastCreatedSkinTime.value + lastCreatedSkinTime.value ) ).collect { result -> result.onSuccess { - _skins.emit(it.skins) - _hasNextSkins.emit(it.hasNext) - _lastCreatedSkinTime.emit(it.skins.last().createdAt) + _hasNextSkins.value = it.hasNext + _skins.emit(skins.value + it.skins) + if (skins.value.isNotEmpty()) { + _lastCreatedSkinTime.value = skins.value.last().createdAt + } }.onFail { _skinEvents.emit(SkinViewModel.SkinEvent.ShowToastMessage("스킨 불러오기 실패.")) } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/adapter/SkinMotionRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/adapter/SkinMotionRVA.kt new file mode 100644 index 000000000..955f09947 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/adapter/SkinMotionRVA.kt @@ -0,0 +1,68 @@ +package com.droidblossom.archive.presentation.ui.skin.adapter + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemSkinMotionBinding +import com.droidblossom.archive.domain.model.capsule_skin.SkinMotion + +class SkinMotionRVA(val ItemClick: (previousPosition: Int?, currentPosition: Int) -> Unit) : ListAdapter(differ) { + + private var previousClickedPosition: Int? = null + + inner class ItemViewHolder( + private val binding: ItemSkinMotionBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: SkinMotion) { + binding.item = data + binding.root.setOnClickListener { + val currentClickedPosition = bindingAdapterPosition + if (currentClickedPosition != RecyclerView.NO_POSITION) { + ItemClick(previousClickedPosition, currentClickedPosition) + previousClickedPosition?.let { previousClickedPosition -> notifyItemChanged(previousClickedPosition) } + notifyItemChanged(currentClickedPosition) + previousClickedPosition = currentClickedPosition + } + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ItemViewHolder { + return ItemViewHolder( + ItemSkinMotionBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun submitList(list: List?) { + previousClickedPosition = null + super.submitList(list) + } + + + + companion object { + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SkinMotion, newItem: SkinMotion): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: SkinMotion, newItem: SkinMotion): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeFragment.kt index 53edee794..ef458ea2e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeFragment.kt @@ -14,18 +14,14 @@ import androidx.navigation.NavController import androidx.navigation.Navigation import com.droidblossom.archive.R import com.droidblossom.archive.databinding.FragmentSkinMakeBinding -import com.droidblossom.archive.domain.model.common.FileName -import com.droidblossom.archive.domain.model.s3.S3UrlRequest import com.droidblossom.archive.presentation.base.BaseFragment -import com.droidblossom.archive.presentation.ui.MainActivity -import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleActivity -import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleViewModel -import com.droidblossom.archive.presentation.ui.home.createcapsule.dialog.DatePickerDialogFragment +import com.droidblossom.archive.presentation.ui.skin.adapter.SkinMotionRVA import com.droidblossom.archive.util.FileUtils import com.droidblossom.archive.util.S3Util import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Date @@ -40,6 +36,12 @@ class SkinMakeFragment : BaseFragment + viewModel.selectSkinMotion(previousPosition, currentPosition) + } + } + private val picMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {uri -> if (uri != null){ viewModel.skinImgUri.value = uri @@ -60,10 +62,23 @@ class SkinMakeFragment : BaseFragment { navController.navigate(R.id.action_skinMakeFragment_to_skinMakeSuccessFragment) } + is SkinMakeViewModel.SkinMakeEvent.DismissLoading -> { + dismissLoading() + } + else -> {} + } } } } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.skinMotions.collect { + skinMotionRVA.submitList(it) + } + } + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -72,6 +87,7 @@ class SkinMakeFragment : BaseFragment val addMotion : StateFlow val skinImgFile: StateFlow + val skinMotions: StateFlow> + val skinMotionIndex: StateFlow fun skinMakeEvent(event: SkinMakeEvent) fun selectAddMotion() fun setFile(skinImgFile: File) fun makeSkin() + fun selectSkinMotion(previousPosition : Int?, currentPosition : Int) + sealed class SkinMakeEvent { object SuccessSkinMake : SkinMakeEvent() + + object DismissLoading : SkinMakeEvent() data class ShowToastMessage(val message : String) : SkinMakeEvent() } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeViewModelImpl.kt index 418d35022..d319f6b4e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeViewModelImpl.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/skin/skinmake/SkinMakeViewModelImpl.kt @@ -4,6 +4,7 @@ import android.net.Uri import android.util.Log import androidx.lifecycle.viewModelScope import com.droidblossom.archive.domain.model.capsule_skin.CapsuleSkinsMakeRequest +import com.droidblossom.archive.domain.model.capsule_skin.SkinMotion import com.droidblossom.archive.domain.model.s3.S3UrlRequest import com.droidblossom.archive.domain.usecase.capsule_skin.CapsuleSkinsMakeUseCase import com.droidblossom.archive.domain.usecase.s3.S3UrlsGetUseCase @@ -11,6 +12,8 @@ import com.droidblossom.archive.presentation.base.BaseViewModel import com.droidblossom.archive.presentation.ui.auth.AuthViewModel import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleViewModel import com.droidblossom.archive.presentation.ui.home.createcapsule.CreateCapsuleViewModelImpl +import com.droidblossom.archive.util.Motion +import com.droidblossom.archive.util.Retarget import com.droidblossom.archive.util.S3Util import com.droidblossom.archive.util.onFail import com.droidblossom.archive.util.onSuccess @@ -42,7 +45,8 @@ class SkinMakeViewModelImpl @Inject constructor( } private val _skinMakeEvents = MutableSharedFlow() - override val skinMakeEvents: SharedFlow = _skinMakeEvents.asSharedFlow() + override val skinMakeEvents: SharedFlow = + _skinMakeEvents.asSharedFlow() override val skinImgUri = MutableStateFlow(null) @@ -51,6 +55,68 @@ class SkinMakeViewModelImpl @Inject constructor( private val _skinImgFile = MutableStateFlow(null) override val skinImgFile: StateFlow = _skinImgFile + + private val _skinMotions = MutableStateFlow>( + listOf( + SkinMotion( + 1L, + "https://github.com/comst19/GradProjectHub/blob/main/db7457be-b654-48ac-8b20-b47c3644fcab_1.gif?raw=true", + Motion.JUMPING_JACKS, + Retarget.CMU, + false + ), + SkinMotion( + 2L, + "https://github.com/comst19/GradProjectHub/blob/main/7a87f9b0-19dd-4ae8-b88e-3e1e142c5122_2.gif?raw=true", + Motion.DAB, + Retarget.FAIR, + false + ), + SkinMotion( + 3L, + "https://github.com/comst19/GradProjectHub/blob/main/45696698-7cad-4838-a947-2b07cb5b2c05_3.gif?raw=true", + Motion.JUMPING, + Retarget.FAIR, + false + ), + SkinMotion( + 4L, + "https://github.com/comst19/GradProjectHub/blob/main/b8112cd6-48fb-42de-b326-80efa4d20150_4.gif?raw=true", + Motion.WAVE_HELLO, + Retarget.FAIR, + false + ), + SkinMotion( + 5L, + "https://github.com/comst19/GradProjectHub/blob/main/02a9fb2b-f126-461c-886a-5f232e93c12b_5.gif?raw=true", + Motion.ZOMBIE, + Retarget.FAIR, + false + ), + SkinMotion( + 6L, + "https://github.com/comst19/GradProjectHub/blob/main/fd145d45-ba3b-41b3-9e58-6f089a4267a0_6.gif?raw=true", + Motion.JESSE_DANCE, + Retarget.ROKOKO, + false + ) + ) + ) + override val skinMotions get() = _skinMotions + + private val _skinMotionIndex = MutableStateFlow(-1) + override val skinMotionIndex: StateFlow + get() = _skinMotionIndex + override fun selectSkinMotion(previousPosition: Int?, currentPosition: Int) { + viewModelScope.launch { + val newList = _skinMotions.value + previousPosition?.let { + newList[it].isClicked = false + } + newList[currentPosition].isClicked = true + _skinMotions.emit(newList) + } + } override fun skinMakeEvent(event: SkinMakeViewModel.SkinMakeEvent) { viewModelScope.launch { _skinMakeEvents.emit(event) @@ -61,6 +127,13 @@ class SkinMakeViewModelImpl @Inject constructor( override fun selectAddMotion() { viewModelScope.launch { + if (addMotion.value) { + val newList = skinMotions.value.map { skinMotion -> + skinMotion.copy(isClicked = false) + } + _skinMotions.emit(newList) + _skinMotionIndex.emit(-1) + } _addMotion.emit(!addMotion.value) } } @@ -87,6 +160,7 @@ class SkinMakeViewModelImpl @Inject constructor( Log.d("getUploadUrls", "$it") uploadFilesToS3(skinImgFile.value!!, it.preSignedImageUrls[0]) }.onFail { + skinMakeEvent(SkinMakeViewModel.SkinMakeEvent.DismissLoading) Log.d("getUploadUrls", "getUploadUrl 실패") } } @@ -112,26 +186,31 @@ class SkinMakeViewModelImpl @Inject constructor( if (uploadSuccess) { submitSkin() } else { - + skinMakeEvent(SkinMakeViewModel.SkinMakeEvent.DismissLoading) } } } - private fun submitSkin(){ + private fun submitSkin() { + if (addMotion.value) { + val skinMotion = skinMotions.value.find { it.isClicked } + _skinMotionIndex.value = if (skinMotion != null) skinMotions.value.indexOf(skinMotion) else -1 + } viewModelScope.launch { capsuleSkinsMakeUseCase( CapsuleSkinsMakeRequest( skinName.value, skinImgFile.value!!.name, S3DIRECTORY, - null, - null + skinMotions.value.getOrNull(skinMotionIndex.value)?.motionName, + skinMotions.value.getOrNull(skinMotionIndex.value)?.retarget ).toDto() - ).collect{ result -> + ).collect { result -> result.onSuccess { skinMakeEvent(SkinMakeViewModel.SkinMakeEvent.SuccessSkinMake) Log.d("스킨 생성", "생성 성공") }.onFail { + skinMakeEvent(SkinMakeViewModel.SkinMakeEvent.DismissLoading) Log.d("스킨 생성", "생성 실패") } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialFragment.kt index 070c617f6..517e99604 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialFragment.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialFragment.kt @@ -1,27 +1,56 @@ package com.droidblossom.archive.presentation.ui.social import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.viewModels import com.droidblossom.archive.R -import com.droidblossom.archive.presentation.ui.skin.SkinFragment +import com.droidblossom.archive.databinding.FragmentSocialBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.social.adapter.SocialVPA +import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint -class SocialFragment : Fragment() { +@AndroidEntryPoint +class SocialFragment : BaseFragment(R.layout.fragment_social) { - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_social, container, false) + override val viewModel: SocialViewModelImpl by viewModels() + + private val socialVPA by lazy { + SocialVPA(this) + } + override fun observeData() { + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val layoutParams = binding.viewHeaderTitle.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin += getStatusBarHeight() + binding.viewHeaderTitle.layoutParams = layoutParams + + initView() } + private fun initView(){ + with(binding){ + socialViewPager.adapter = socialVPA + + TabLayoutMediator(socialTabLayout, socialViewPager) { tab, position -> + tab.text = when (position) { + 0 -> getString(R.string.group) + 1 -> getString(R.string.friend) + else -> null + } + }.attach() + } + } companion object{ const val TAG = "Social" fun newIntent()= SocialFragment() } + } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialViewModel.kt new file mode 100644 index 000000000..27d3382a0 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialViewModel.kt @@ -0,0 +1,4 @@ +package com.droidblossom.archive.presentation.ui.social + +interface SocialViewModel { +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialViewModelImpl.kt new file mode 100644 index 000000000..ce8c0d7c5 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/SocialViewModelImpl.kt @@ -0,0 +1,12 @@ +package com.droidblossom.archive.presentation.ui.social + +import com.droidblossom.archive.presentation.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SocialViewModelImpl @Inject constructor( + +): BaseViewModel(), SocialViewModel { + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/SocialFriendCapsuleRVA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/SocialFriendCapsuleRVA.kt new file mode 100644 index 000000000..7d480bb00 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/SocialFriendCapsuleRVA.kt @@ -0,0 +1,85 @@ +package com.droidblossom.archive.presentation.ui.social.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.databinding.ItemSocialCapsuleCloseBinding +import com.droidblossom.archive.databinding.ItemSocialCapsuleOpenBinding +import com.droidblossom.archive.domain.model.common.SocialCapsules +import com.droidblossom.archive.presentation.ui.home.HomeFragment + +class SocialFriendCapsuleRVA( + private val openCapsuleClick: (Long) -> Unit, + private val closeCapsuleClick:() -> Unit +) : ListAdapter(differ) { + + inner class OpenedCapsuleViewHolder( + private val binding: ItemSocialCapsuleOpenBinding + ) : RecyclerView.ViewHolder(binding.root){ + + fun bindOpenedCapsule(data : SocialCapsules){ + binding.item = data + binding.root.setOnClickListener { + openCapsuleClick(data.capsuleId) + } + } + + } + + inner class ClosedCapsuleViewHolder( + private val binding: ItemSocialCapsuleCloseBinding + ) : RecyclerView.ViewHolder(binding.root){ + + fun bindClosedCapsule(data: SocialCapsules){ + binding.item = data + binding.root.setOnClickListener{ + closeCapsuleClick() + } + } + + } + + override fun getItemViewType(position: Int): Int { + return if (getItem(position).isOpened) { + TYPE_OPENED_CAPSULE + } else { + TYPE_CLOSED_CAPSULE + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + TYPE_OPENED_CAPSULE -> OpenedCapsuleViewHolder(ItemSocialCapsuleOpenBinding.inflate(inflater, parent, false)) + TYPE_CLOSED_CAPSULE -> ClosedCapsuleViewHolder(ItemSocialCapsuleCloseBinding.inflate(inflater, parent, false)) + else -> throw IllegalArgumentException("Invalid view type") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = getItem(position) + when (holder) { + is OpenedCapsuleViewHolder -> holder.bindOpenedCapsule(item) + is ClosedCapsuleViewHolder -> holder.bindClosedCapsule(item) + } + } + + + companion object { + + private const val TYPE_OPENED_CAPSULE = 0 + private const val TYPE_CLOSED_CAPSULE = 1 + + val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SocialCapsules, newItem: SocialCapsules): Boolean { + return oldItem.capsuleId == newItem.capsuleId + } + + override fun areContentsTheSame(oldItem: SocialCapsules, newItem: SocialCapsules): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/SocialVPA.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/SocialVPA.kt new file mode 100644 index 000000000..e495277e8 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/SocialVPA.kt @@ -0,0 +1,20 @@ +package com.droidblossom.archive.presentation.ui.social.adapter + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.droidblossom.archive.presentation.ui.social.page.friend.SocialFriendFragment +import com.droidblossom.archive.presentation.ui.social.page.group.SocialGroupFragment + +class SocialVPA(fragment: Fragment) : FragmentStateAdapter(fragment){ + + private val fragmentList = listOf( + SocialGroupFragment(), + SocialFriendFragment() + ) + + override fun getItemCount(): Int = fragmentList.size + + override fun createFragment(position: Int): Fragment { + return fragmentList[position] + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/TestSocialFriendModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/TestSocialFriendModel.kt new file mode 100644 index 000000000..7c40e45c0 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/adapter/TestSocialFriendModel.kt @@ -0,0 +1,12 @@ +package com.droidblossom.archive.presentation.ui.social.adapter + +data class TestSocialFriendModel( + val id:Long, + val capsuleTitle:String, + val capsuleWriter:String, + val capsuleContent:String, + val capsuleContentImg:String, + val capsuleLocation:String, + val capsuleCreateTime:String, + val isOpened:Boolean +) diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendFragment.kt new file mode 100644 index 000000000..903af635f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendFragment.kt @@ -0,0 +1,208 @@ +package com.droidblossom.archive.presentation.ui.social.page.friend + +import android.annotation.SuppressLint +import android.graphics.Rect +import android.os.Bundle +import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSocialFriendBinding +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.capsule.CapsuleDetailActivity +import com.droidblossom.archive.presentation.ui.home.HomeFragment +import com.droidblossom.archive.presentation.ui.social.adapter.SocialFriendCapsuleRVA +import com.droidblossom.archive.util.SpaceItemDecoration +import com.droidblossom.archive.util.updateTopConstraintsForSearch +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + + +@AndroidEntryPoint +class SocialFriendFragment : BaseFragment(R.layout.fragment_social_friend) { + + override val viewModel: SocialFriendViewModelImpl by viewModels() + + private val socialFriendCapsuleRVA by lazy { + SocialFriendCapsuleRVA( + { id -> + startActivity( + CapsuleDetailActivity.newIntent( + requireContext(), + id, + HomeFragment.CapsuleType.PUBLIC + ) + ) + }, + { + showToastMessage("개봉되지 않은 캡슐입니다.") + } + ) + } + + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.isSearchOpen.collect { + val layoutParams = binding.socialFriendSwipeRefreshLayout.layoutParams as ConstraintLayout.LayoutParams + layoutParams.updateTopConstraintsForSearch( + isSearchOpen = it, + searchOpenView = binding.searchOpenBtn, + searchView = binding.searchBtn, + additionalMarginDp = 16f, + resources = resources + ) + if (it){ + binding.searchOpenEditT.requestFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.showSoftInput(binding.searchOpenEditT, InputMethodManager.SHOW_IMPLICIT); + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.socialFriendEvents.collect { event -> + when (event) { + is SocialFriendViewModel.SocialFriendEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + is SocialFriendViewModel.SocialFriendEvent.HideLoading -> { + binding.socialFriendSwipeRefreshLayout.isRefreshing = false + } + + else -> {} + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.publicCapsules.collect{ publicCapsules -> + if (viewModel.clearCapsule){ + viewModel.clearCapsule = false + }else{ + socialFriendCapsuleRVA.submitList(publicCapsules){ + if (binding.socialFriendSwipeRefreshLayout.isRefreshing){ + binding.socialFriendSwipeRefreshLayout.isRefreshing = false + binding.socialFriendRV.scrollToPosition(0) + } + } + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.socialFriendEvents.collect{ event-> + when (event){ + is SocialFriendViewModel.SocialFriendEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + is SocialFriendViewModel.SocialFriendEvent.HideLoading ->{ + binding.socialFriendSwipeRefreshLayout.isRefreshing = false + } + else -> { + + } + } + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initRVA() + initSearchEdit() + } + + private fun initRVA() { + binding.socialFriendRV.adapter = socialFriendCapsuleRVA + + val spaceInPixels = resources.getDimensionPixelSize(R.dimen.margin) + binding.socialFriendRV.addItemDecoration(SpaceItemDecoration(spaceBottom = spaceInPixels)) + binding.socialFriendSwipeRefreshLayout.setOnRefreshListener { + viewModel.getLatestPublicCapsule() + } + + binding.socialFriendRV.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + + if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_DRAGGING) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + if (totalItemCount - lastVisibleItemPosition <= 5) { + viewModel.onScrollNearBottom() + } + } + } + }) + } + + @SuppressLint("ClickableViewAccessibility") + fun initSearchEdit(){ + binding.searchOpenEditT.setOnEditorActionListener { _, i, _ -> + if (i == EditorInfo.IME_ACTION_DONE) { + if (!binding.searchOpenEditT.text.isNullOrEmpty()) { + viewModel.searchFriendCapsule() + } + true + } + false + } + binding.searchOpenEditT.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + viewModel.closeSearchFriendCapsule() + } + } + binding.root.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val focusedView = binding.searchOpenBtn + val outRect = Rect() + focusedView.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + focusedView.clearFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(focusedView.windowToken, 0) + } + } + false + } + + binding.socialFriendRV.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val focusedView = binding.searchOpenBtn + val outRect = Rect() + focusedView.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + focusedView.clearFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(focusedView.windowToken, 0) + } + } + false + } + + binding.searchOpenBtnT.setOnClickListener { + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0) + } + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendViewModel.kt new file mode 100644 index 000000000..32447acbf --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendViewModel.kt @@ -0,0 +1,30 @@ +package com.droidblossom.archive.presentation.ui.social.page.friend + +import com.droidblossom.archive.domain.model.common.SocialCapsules +import com.droidblossom.archive.presentation.ui.mypage.MyPageViewModel +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface SocialFriendViewModel { + val socialFriendEvents : SharedFlow + val publicCapsules : StateFlow> + val isSearchOpen : StateFlow + val hasNextPage : StateFlow + val lastCreatedTime : StateFlow + var clearCapsule:Boolean + + + fun socialFriendEvent(event: SocialFriendEvent) + fun openSearchFriendCapsule() + fun closeSearchFriendCapsule() + + fun searchFriendCapsule() + fun getPublicCapsulePage() + fun onScrollNearBottom() + fun getLatestPublicCapsule() + sealed class SocialFriendEvent{ + data class ShowToastMessage(val message : String) : SocialFriendEvent() + object HideLoading : SocialFriendEvent() + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendViewModelImpl.kt new file mode 100644 index 000000000..3122a2e25 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/friend/SocialFriendViewModelImpl.kt @@ -0,0 +1,123 @@ +package com.droidblossom.archive.presentation.ui.social.page.friend + +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.data.dto.common.PagingRequestDto +import com.droidblossom.archive.domain.model.common.SocialCapsules +import com.droidblossom.archive.domain.usecase.open.PublicCapsulePageUseCase +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.util.DateUtils +import com.droidblossom.archive.util.onFail +import com.droidblossom.archive.util.onSuccess +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class SocialFriendViewModelImpl @Inject constructor( + private val publicCapsulePageUseCase: PublicCapsulePageUseCase +) : BaseViewModel(), SocialFriendViewModel { + + private val _socialFriendEvents = MutableSharedFlow() + override val socialFriendEvents: SharedFlow + get() = _socialFriendEvents.asSharedFlow() + + private val _publicCapsules = MutableStateFlow(listOf()) + override val publicCapsules: StateFlow> + get() = _publicCapsules + + private val _isSearchOpen = MutableStateFlow(false) + override val isSearchOpen: StateFlow + get() = _isSearchOpen + + private val _hasNextPage = MutableStateFlow(true) + override val hasNextPage: StateFlow + get() = _hasNextPage + private val _lastCreatedTime = MutableStateFlow(DateUtils.dataServerString) + override val lastCreatedTime: StateFlow + get() = _lastCreatedTime + override var clearCapsule = false + + private val scrollEventChannel = Channel(Channel.CONFLATED) + private val scrollEventFlow = + scrollEventChannel.receiveAsFlow().throttleFirst(1000, TimeUnit.MILLISECONDS) + + private var getSecretCapsuleListJob: Job? = null + + + init { + viewModelScope.launch { + scrollEventFlow.collect { + getPublicCapsulePage() + } + } + getPublicCapsulePage() + } + + override fun onScrollNearBottom() { + scrollEventChannel.trySend(Unit) + } + + override fun socialFriendEvent(event: SocialFriendViewModel.SocialFriendEvent) { + viewModelScope.launch { + _socialFriendEvents.emit(event) + } + } + + override fun openSearchFriendCapsule() { + viewModelScope.launch { + _isSearchOpen.emit(true) + } + } + + override fun closeSearchFriendCapsule() { + viewModelScope.launch { + _isSearchOpen.emit(false) + } + } + + + override fun getPublicCapsulePage() { + if (hasNextPage.value){ + getSecretCapsuleListJob?.cancel() + getSecretCapsuleListJob = viewModelScope.launch { + publicCapsulePageUseCase( + PagingRequestDto( + 15, + lastCreatedTime.value + ) + ).collect { result -> + result.onSuccess { + _hasNextPage.value = it.hasNext + _publicCapsules.emit(publicCapsules.value + it.publicCapsules) + _lastCreatedTime.value = publicCapsules.value.last().createdDate + }.onFail { + socialFriendEvent(SocialFriendViewModel.SocialFriendEvent.ShowToastMessage("공개캡슐 불러오기 실패")) + } + } + socialFriendEvent(SocialFriendViewModel.SocialFriendEvent.HideLoading) + } + } + } + + override fun getLatestPublicCapsule() { + clearCapsule = true + _publicCapsules.value = listOf() + _hasNextPage.value = true + _lastCreatedTime.value = DateUtils.dataServerString + getPublicCapsulePage() + } + + override fun searchFriendCapsule() { + + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupFragment.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupFragment.kt new file mode 100644 index 000000000..33d860bb2 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupFragment.kt @@ -0,0 +1,204 @@ +package com.droidblossom.archive.presentation.ui.social.page.group + +import android.annotation.SuppressLint +import android.graphics.Rect +import android.os.Bundle +import android.util.Log +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.FragmentSocialGroupBinding +import com.droidblossom.archive.domain.model.common.SocialCapsules +import com.droidblossom.archive.presentation.base.BaseFragment +import com.droidblossom.archive.presentation.ui.capsule.CapsuleDetailActivity +import com.droidblossom.archive.presentation.ui.home.HomeFragment +import com.droidblossom.archive.presentation.ui.social.adapter.SocialFriendCapsuleRVA +import com.droidblossom.archive.presentation.ui.social.adapter.TestSocialFriendModel +import com.droidblossom.archive.presentation.ui.social.page.friend.SocialFriendViewModel +import com.droidblossom.archive.util.SpaceItemDecoration +import com.droidblossom.archive.util.updateTopConstraintsForSearch +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SocialGroupFragment : BaseFragment(R.layout.fragment_social_group) { + + override val viewModel: SocialGroupViewModelImpl by viewModels() + + private val socialFriendCapsuleRVA by lazy { + SocialFriendCapsuleRVA( + { id -> + startActivity( + CapsuleDetailActivity.newIntent( + requireContext(), + id, + HomeFragment.CapsuleType.GROUP + ) + ) + }, + { + showToastMessage("개봉되지 않은 캡슐입니다.") + } + ) + } + override fun observeData() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.isSearchOpen.collect { + val layoutParams = binding.socialFriendSwipeRefreshLayout.layoutParams as ConstraintLayout.LayoutParams + layoutParams.updateTopConstraintsForSearch( + isSearchOpen = it, + searchOpenView = binding.searchOpenBtn, + searchView = binding.searchBtn, + additionalMarginDp = 16f, + resources = resources + ) + if (it){ + binding.searchOpenEditT.requestFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.showSoftInput(binding.searchOpenEditT, InputMethodManager.SHOW_IMPLICIT); + + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.groupCapsules.collect{ groupCapsules -> + if (viewModel.clearCapsule){ + viewModel.clearCapsule = false + }else{ + socialFriendCapsuleRVA.submitList(groupCapsules){ + if (binding.socialFriendSwipeRefreshLayout.isRefreshing){ + binding.socialFriendSwipeRefreshLayout.isRefreshing = false + binding.socialGroupRV.scrollToPosition(0) + } + } + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.socialGroupEvents.collect{ event-> + when (event){ + is SocialGroupViewModel.SocialGroupEvent.ShowToastMessage -> { + showToastMessage(event.message) + } + + is SocialGroupViewModel.SocialGroupEvent.HideLoading ->{ + binding.socialFriendSwipeRefreshLayout.isRefreshing = false + } + else -> { + + } + } + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + initRVA() + initSearchEdit() + } + + private fun initRVA() { + binding.socialGroupRV.adapter = socialFriendCapsuleRVA + val spaceInPixels = resources.getDimensionPixelSize(R.dimen.margin) + binding.socialGroupRV.addItemDecoration(SpaceItemDecoration(spaceBottom = spaceInPixels)) + binding.socialFriendSwipeRefreshLayout.setOnRefreshListener { + viewModel.getLatestGroupCapsule() + } + + binding.socialGroupRV.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + + if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_DRAGGING) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + if (totalItemCount - lastVisibleItemPosition <= 5) { + viewModel.getGroupCapsulePage() + } + } + } + }) + } + + + @SuppressLint("ClickableViewAccessibility") + fun initSearchEdit(){ + binding.searchOpenEditT.setOnEditorActionListener { _, i, _ -> + if (i == EditorInfo.IME_ACTION_DONE) { + if (!binding.searchOpenEditT.text.isNullOrEmpty()) { + viewModel.searchGroupCapsule() + } + true + } + false + } + binding.searchOpenEditT.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + viewModel.closeSearchGroupCapsule() + } + } + binding.root.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val focusedView = binding.searchOpenBtn + val outRect = Rect() + focusedView.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + focusedView.clearFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(focusedView.windowToken, 0) + } + } + false + } + + binding.socialGroupRV.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val focusedView = binding.searchOpenBtn + val outRect = Rect() + focusedView.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + focusedView.clearFocus() + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(focusedView.windowToken, 0) + } + } + false + } + + binding.searchOpenBtnT.setOnClickListener { + val imm = requireActivity().getSystemService(InputMethodManager::class.java) + imm.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0) + } + } + + private fun updateRecyclerViewConstraints(isSearchOpen: Boolean) { + val layoutParams = binding.socialGroupRV.layoutParams as ConstraintLayout.LayoutParams + layoutParams.topToBottom = if (isSearchOpen) binding.searchOpenBtn.id else binding.searchBtn.id + val additionalMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics).toInt() + layoutParams.topMargin = additionalMargin + binding.socialGroupRV.layoutParams = layoutParams + } + + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupViewModel.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupViewModel.kt new file mode 100644 index 000000000..f3555d83f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupViewModel.kt @@ -0,0 +1,33 @@ +package com.droidblossom.archive.presentation.ui.social.page.group + +import com.droidblossom.archive.domain.model.common.SocialCapsules +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface SocialGroupViewModel { + + val socialGroupEvents : SharedFlow + val groupCapsules : StateFlow> + val isSearchOpen : StateFlow + val hasNextPage : StateFlow + val lastCreatedTime : StateFlow + var clearCapsule:Boolean + + fun socialGroupEvent(event: SocialGroupEvent) + + + fun openSearchGroupCapsule() + fun closeSearchGroupCapsule() + + fun searchGroupCapsule() + + fun getGroupCapsulePage() + + fun getLatestGroupCapsule() + + sealed class SocialGroupEvent{ + data class ShowToastMessage(val message : String) : SocialGroupEvent() + object HideLoading : SocialGroupEvent() + + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupViewModelImpl.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupViewModelImpl.kt new file mode 100644 index 000000000..fe8d78c22 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/social/page/group/SocialGroupViewModelImpl.kt @@ -0,0 +1,84 @@ +package com.droidblossom.archive.presentation.ui.social.page.group + +import androidx.lifecycle.viewModelScope +import com.droidblossom.archive.domain.model.common.SocialCapsules +import com.droidblossom.archive.presentation.base.BaseViewModel +import com.droidblossom.archive.presentation.ui.social.page.friend.SocialFriendViewModel +import com.droidblossom.archive.util.DateUtils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SocialGroupViewModelImpl @Inject constructor( + +): BaseViewModel(), SocialGroupViewModel { + + private val _socialGroupEvents = MutableSharedFlow() + override val socialGroupEvents: SharedFlow + get() =_socialGroupEvents.asSharedFlow() + + private val _isSearchOpen = MutableStateFlow(false) + override val isSearchOpen: StateFlow + get() = _isSearchOpen + + private val _groupCapsules = MutableStateFlow(listOf()) + override val groupCapsules: StateFlow> + get() = _groupCapsules + + private val _hasNextPage = MutableStateFlow(true) + override val hasNextPage: StateFlow + get() = _hasNextPage + private val _lastCreatedTime = MutableStateFlow(DateUtils.dataServerString) + override val lastCreatedTime: StateFlow + get() = _lastCreatedTime + override var clearCapsule = false + + init { + //getGroupCapsulePage() + } + + override fun socialGroupEvent(event: SocialGroupViewModel.SocialGroupEvent) { + viewModelScope.launch { + _socialGroupEvents.emit(event) + } + } + + override fun openSearchGroupCapsule() { + viewModelScope.launch { + _isSearchOpen.emit(true) + } + } + + override fun closeSearchGroupCapsule() { + viewModelScope.launch { + _isSearchOpen.emit(false) + } + } + + override fun searchGroupCapsule(){ + + } + + override fun getGroupCapsulePage(){ + viewModelScope.launch { + if (hasNextPage.value) { + + } + } + } + + override fun getLatestGroupCapsule(){ + clearCapsule = true + _groupCapsules.value = listOf() + _hasNextPage.value = true + _lastCreatedTime.value = DateUtils.dataServerString + getGroupCapsulePage() + } + +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/splash/SplashActivity.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/splash/SplashActivity.kt index f32534774..aa68d6963 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/splash/SplashActivity.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/presentation/ui/splash/SplashActivity.kt @@ -1,58 +1,88 @@ package com.droidblossom.archive.presentation.ui.splash -import android.content.Intent +import android.Manifest import android.os.Bundle import android.util.Log -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope import com.droidblossom.archive.R +import com.droidblossom.archive.databinding.ActivitySplashBinding +import com.droidblossom.archive.presentation.base.BaseActivity +import com.droidblossom.archive.presentation.customview.PermissionDialogButtonClickListener +import com.droidblossom.archive.presentation.customview.PermissionDialogFragment import com.droidblossom.archive.presentation.ui.MainActivity +import com.droidblossom.archive.presentation.ui.MainViewModelImpl import com.droidblossom.archive.presentation.ui.auth.AuthActivity -import com.droidblossom.archive.presentation.ui.skin.SkinFragment import com.droidblossom.archive.util.DataStoreUtils -import com.droidblossom.archive.util.MyFirebaseMessagingService import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint -class SplashActivity : AppCompatActivity() { +class SplashActivity : BaseActivity(R.layout.activity_splash) { + @Inject lateinit var dataStoreUtils: DataStoreUtils + override val viewModel: Nothing? = null + override fun observeData() {} + + lateinit var viewBinding: ActivitySplashBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_splash) + + viewBinding = binding // dataStoreUtils 사용 lifecycleScope.launch { delay(1000) if (dataStoreUtils.fetchAccessToken().isNotEmpty() && dataStoreUtils.fetchRefreshToken().isNotEmpty()) { - MainActivity.goMain(this@SplashActivity) + essentialPermissionLauncher.launch(essentialPermissionList) } else { AuthActivity.goAuth(this@SplashActivity) finish() } } -// handleIntent(intent) } -// override fun onNewIntent(intent: Intent?) { -// super.onNewIntent(intent) -// intent?.let { -// setIntent(it) -// handleIntent(it) -// } -// } -// private fun handleIntent(intent: Intent) { -// val destination = intent.getStringExtra("fragmentDestination") -// Log.d("어디로","스플 - 어디로 가야하오 데스티네이션: $destination") -// -// intent.extras?.let { bundle -> -// val nick = bundle.getString("click_action") -// Log.d("어디로","스플 - 어디로 가야하오 엑스트라: $nick") -// } -// -// } + + private val essentialPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.all { it.value } -> { + MainActivity.goMain(this@SplashActivity) + } + + else -> { + handleEssentialPermissionsDenied() + } + } + + } + + private fun handleEssentialPermissionsDenied() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) || shouldShowRequestPermissionRationale( + Manifest.permission.ACCESS_COARSE_LOCATION + ) || shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + ) { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + } else { + showSettingsDialog( + PermissionDialogFragment.PermissionType.ESSENTIAL, + object : PermissionDialogButtonClickListener { + override fun onLeftButtonClicked() { + showToastMessage("ARchive 앱을 사용하려면 카메라, 위치 권한은 필수입니다.") + finish() + } + + override fun onRightButtonClicked() { + navigateToAppSettings{essentialPermissionLauncher.launch(essentialPermissionList)} + } + + }) + } + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ApiHandler.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ApiHandler.kt index 528dfe857..43db9418f 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ApiHandler.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ApiHandler.kt @@ -2,6 +2,8 @@ package com.droidblossom.archive.util import com.droidblossom.archive.ARchiveApplication import com.droidblossom.archive.di.RetrofitModule +import com.droidblossom.archive.presentation.model.AppEvent +import org.greenrobot.eventbus.EventBus import retrofit2.Response suspend fun apiHandler( @@ -9,6 +11,7 @@ suspend fun apiHandler( mapper: (T) -> R ): RetrofitResult { if (ARchiveApplication.isOnline().not()) { + EventBus.getDefault().post(AppEvent.NetworkDisconnectedEvent) return RetrofitResult.Error(Exception(RetrofitModule.NETWORK_EXCEPTION_OFFLINE)) } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/BindingAdapter.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/BindingAdapter.kt index f1e30e05e..8667f2bb5 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/BindingAdapter.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/BindingAdapter.kt @@ -5,11 +5,13 @@ import android.annotation.SuppressLint import android.graphics.drawable.Drawable import android.net.Uri import android.telephony.PhoneNumberFormattingTextWatcher +import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.widget.EditText import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.databinding.BindingAdapter @@ -17,10 +19,16 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions import com.droidblossom.archive.R +import com.google.android.material.tabs.TabLayout import de.hdodenhof.circleimageview.CircleImageView import java.text.SimpleDateFormat import java.util.Locale +@BindingAdapter("textInt") +fun AppCompatTextView.textInt(int: Int) { + this.text = int.toString() +} + @BindingAdapter(value = ["bind:imageUrl", "bind:placeholder"], requireAll = false) fun ImageView.setImage(imageUrl: Uri?, placeholder: Drawable?) { Glide.with(this.context) @@ -90,8 +98,13 @@ fun TextView.displayRemainingTime(totalSeconds: Int) { this.text = String.format("%02d분 %02d초", minutes, seconds) } +@BindingAdapter("bind:culcLastDays") +fun TextView.culcLastDays(date: String) { + this.text = DateUtils.calcLastDate(date) +} + @BindingAdapter("bind:animateFAB") -fun CardView.animateFAB(y : Float){ +fun CardView.animateFAB(y: Float) { ObjectAnimator.ofFloat(this, "translationY", y).apply { start() } } @@ -113,7 +126,7 @@ fun TextView.setFormattedDateTime(dateString: String) { @BindingAdapter("bind:displayCreationDateTimeNullFormatted") fun TextView.setFormattedDateTimeNull(dateString: String?) { - if (!dateString.isNullOrEmpty()){ + if (!dateString.isNullOrEmpty()) { try { val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault()) val date = parser.parse(dateString) @@ -124,7 +137,7 @@ fun TextView.setFormattedDateTimeNull(dateString: String?) { } catch (e: Exception) { this.text = "날짜 형식 오류" } - }else{ + } else { this.text = "일반 캡슐 입니다" } } @@ -170,17 +183,80 @@ fun setLayoutHeight(view: View, height: Float) { } @BindingAdapter("bind:setCapsuleType2Img") -fun ImageView.setCapsuleType2Img(type: String?){ - when(type) { +fun ImageView.setCapsuleType2Img(type: String?) { + when (type) { "SECRET" -> { this.setImageResource(R.drawable.ic_secret_marker_24) } + "PUBLIC" -> { this.setImageResource(R.drawable.ic_public_marker_24) } - "GROUP" ->{ + + "GROUP" -> { this.setImageResource(R.drawable.ic_group_marker_24) } + else -> {} } +} + +@BindingAdapter("bind:tabMarginEnd") +fun TabLayout.setTabItemMargin(marginEndDp: Int) { + val marginEndPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, marginEndDp.toFloat(), resources.displayMetrics + ).toInt() + + val tabs = getChildAt(0) as ViewGroup + for (i in 0 until tabs.childCount) { + val tab = tabs.getChildAt(i) + val layoutParams = tab.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.marginEnd = marginEndPx + tab.layoutParams = layoutParams + } + requestLayout() +} + +@BindingAdapter( + value = ["bind:isFriend", "bind:isInviteToMe", "bind:isInviteToFriend", "bind:friendName"], + requireAll = false +) +fun TextView.addFriendText( + isFriend: Boolean, + isInviteToMe: Boolean, + isInviteToFriend: Boolean, + name: String +) { + if (isFriend || isInviteToFriend || isInviteToMe) { + if (isFriend) { + this.text = "이미 친구입니다." + } else if (isInviteToFriend) { + this.text = "요청을 보냈습니다." + } else { + this.text = "요청을 받았습니다,확인해주세요." + } + } else { + this.text = name + } +} + +@SuppressLint("SetTextI18n") +@BindingAdapter(value = ["bind:count", "bind:showDecimal"], requireAll = true) +fun TextView.formatCountWithK(count: Int, showDecimal: Boolean) { + + if (count < 1000) { + this.text = count.toString() + } else { + if (showDecimal) { + val thousands = count / 1000 + val remainder = (count % 1000) / 100 + if (remainder == 0) { + this.text = "${thousands}K" + } else { + this.text = "${thousands}.${remainder}K" + } + } else { + this.text = "${count / 1000}K" + } + } } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ClipboardUtil.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ClipboardUtil.kt new file mode 100644 index 000000000..21c492a19 --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ClipboardUtil.kt @@ -0,0 +1,13 @@ +package com.droidblossom.archive.util + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context + +object ClipboardUtil { + fun copyTextToClipboard(context: Context, label:String, text: String) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + val clip = ClipData.newPlainText(label, text) + clipboard?.setPrimaryClip(clip) + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ContactsUtils.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ContactsUtils.kt new file mode 100644 index 000000000..5ed9de50f --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/ContactsUtils.kt @@ -0,0 +1,44 @@ +package com.droidblossom.archive.util + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.content.Context +import android.provider.ContactsContract +import android.telephony.TelephonyManager +import android.util.Log +import com.droidblossom.archive.data.dto.friend.request.PhoneBooks + + +object ContactsUtils { + + @SuppressLint("MissingPermission") + fun getContacts(context : Context) : List { + val resolver: ContentResolver = context.contentResolver + val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI + + val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val projection = arrayOf( + ContactsContract.CommonDataKinds.Phone.CONTACT_ID, + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Phone.NUMBER + ) + val numberList = mutableListOf() + val cursor = resolver.query(phoneUri, projection, null, null, null) + if (cursor != null) { + while (cursor.moveToNext()) { + val nameIndex = cursor.getColumnIndex(projection[1]) + val numberIndex = cursor.getColumnIndex(projection[2]) + val name = cursor.getString(nameIndex) + var number = cursor.getString(numberIndex) + number = number.replace("-", "") + numberList.add(PhoneBooks(number,name)) + Log.d("GetContact", "이름 : $name 번호 : $number") + } + } + numberList.removeIf { + it.originPhone == tm.line1Number.replace("+82","0") + } + cursor!!.close() + return numberList.toList() + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/CustomLifecycleOwner.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/CustomLifecycleOwner.kt new file mode 100644 index 000000000..69cfebd2c --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/CustomLifecycleOwner.kt @@ -0,0 +1,17 @@ +package com.droidblossom.archive.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +class CustomLifecycleOwner : LifecycleOwner { + + private val _customLifecycle = LifecycleRegistry(this) + + fun handleLifecycleEvent(event: Lifecycle.Event) { + _customLifecycle.handleLifecycleEvent(event) + } + + override val lifecycle: Lifecycle + get() = _customLifecycle +} diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DataStoreUtils.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DataStoreUtils.kt index d05e097d3..eea861dd5 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DataStoreUtils.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DataStoreUtils.kt @@ -7,6 +7,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import com.droidblossom.archive.presentation.ui.MainActivity import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.last @@ -20,6 +21,18 @@ class DataStoreUtils @Inject constructor(private val context: Context) { private val REFRESH_TOKEN_KEY = stringPreferencesKey("RefreshToken") private val FCM_TOKEN_KEY = stringPreferencesKey("FcmToken") private val NOTIFICATIONS_ENABLED_KEY = booleanPreferencesKey("NotificationsEnabled") + private val SELECTED_TAB_KEY = stringPreferencesKey("SelectedTab") + } + + suspend fun saveSelectedTab(tabName: String) { + context.dataStore.edit { preferences -> + preferences[SELECTED_TAB_KEY] = tabName + } + } + + suspend fun fetchSelectedTab(): String { + val preferences = context.dataStore.data.first() + return preferences[SELECTED_TAB_KEY] ?: MainActivity.MainPage.HOME.name } suspend fun saveAccessToken(accessToken: String) { @@ -28,7 +41,7 @@ class DataStoreUtils @Inject constructor(private val context: Context) { preferences[ACCESS_TOKEN_KEY] = encryptedData.toBase64() + ":" + iv.toBase64() } } - + suspend fun fetchAccessToken(): String { val preferences = context.dataStore.data.first() val combined = preferences[ACCESS_TOKEN_KEY] ?: return "" @@ -83,5 +96,8 @@ class DataStoreUtils @Inject constructor(private val context: Context) { } } -private fun ByteArray.toBase64(): String = android.util.Base64.encodeToString(this, android.util.Base64.NO_WRAP) -private fun String.fromBase64(): ByteArray = android.util.Base64.decode(this, android.util.Base64.NO_WRAP) \ No newline at end of file +private fun ByteArray.toBase64(): String = + android.util.Base64.encodeToString(this, android.util.Base64.NO_WRAP) + +private fun String.fromBase64(): ByteArray = + android.util.Base64.decode(this, android.util.Base64.NO_WRAP) \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DateUtils.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DateUtils.kt index e014994b3..675c4630e 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DateUtils.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/DateUtils.kt @@ -1,5 +1,7 @@ package com.droidblossom.archive.util +import android.annotation.SuppressLint +import android.util.Log import java.text.ParseException import java.text.SimpleDateFormat import java.util.Calendar @@ -148,7 +150,18 @@ object DateUtils { @JvmStatic fun main(args: Array) { - println(getAfterYears(1, "yyyyMMdd")) + + } + + @SuppressLint("SimpleDateFormat") + fun calcLastDate(date: String): String { + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") + val date = format.parse(date) + val currentDate = format.parse(dataServerString) + + val difference = currentDate.time - date.time + val daysDifference = difference / (24 * 60 * 60 * 1000) + return if (daysDifference == 0.toLong()) "오늘" else "${daysDifference}일전" } fun getAfterSeconds(date1: Date, date2: Date): Int { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/Expansion.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/Expansion.kt index 9ce4949f4..96e6b8db4 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/Expansion.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/Expansion.kt @@ -2,8 +2,12 @@ package com.droidblossom.archive.util import android.app.Activity import android.content.Intent +import android.content.res.Resources import android.os.Build +import android.util.TypedValue +import android.view.View import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout import java.io.Serializable fun Intent.intentSerializable(key: String, clazz: Class): T? { @@ -17,4 +21,17 @@ fun Intent.intentSerializable(key: String, clazz: Class): T fun Activity.getStatusBarHeight(): Int { val resourceId = this.resources.getIdentifier("status_bar_height", "dimen", "android") return if (resourceId > 0) this.resources.getDimensionPixelSize(resourceId) else 0 +} + +fun ConstraintLayout.LayoutParams.updateTopConstraintsForSearch( + isSearchOpen: Boolean, + searchOpenView: View, + searchView: View, + additionalMarginDp: Float, + resources: Resources +) { + this.topToBottom = if (isSearchOpen) searchOpenView.id else searchView.id + val additionalMargin = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, additionalMarginDp, resources.displayMetrics).toInt() + this.topMargin = additionalMargin } \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/MyFirebaseMessagingService.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/MyFirebaseMessagingService.kt index c8cba0ea9..2bfa6240b 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/MyFirebaseMessagingService.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/MyFirebaseMessagingService.kt @@ -12,7 +12,6 @@ import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import com.droidblossom.archive.R -import com.droidblossom.archive.presentation.snack.CallSnackBar import com.droidblossom.archive.presentation.ui.MainActivity import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessagingService @@ -49,34 +48,37 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { if (remoteMessage.data.isNotEmpty()){ EventBus.getDefault().post(remoteMessage.data) - CoroutineScope(Dispatchers.IO).launch { - val notificationsEnabled = dataStoreUtils.fetchNotificationsEnabled() + handleNotification(remoteMessage) - if (notificationsEnabled) { - handleNotification(remoteMessage) - } else { - Log.d(TAG, "사용자가 알림을 비활성화했습니다.") - } - } }else{ // 데이터 메시지 비어있음 Log.d(TAG, "메시지를 수신하지 못했습니다.") } - } private fun handleNotification(remoteMessage: RemoteMessage) { var channelName = "공지사항" when(remoteMessage.data["topic"]){ - FcmTopic.CAPSULE_SKIN.toString() -> { + FcmTopic.CAPSULE_SKIN.name -> { channelName = "캡슐 스킨 생성" + sendNotification(remoteMessage, FcmTopic.CAPSULE_SKIN.name, channelName) + } + + FcmTopic.FRIEND_REQUEST.name -> { + channelName = "친구 요청" + sendNotification(remoteMessage, FcmTopic.FRIEND_REQUEST.name, channelName) + } + + FcmTopic.FRIEND_ACCEPT.name -> { + channelName = "친구 요청 수락" + sendNotification(remoteMessage, FcmTopic.FRIEND_ACCEPT.name, channelName) } - else -> {} + else -> {} } - sendNotification(remoteMessage, FcmTopic.CAPSULE_SKIN.toString(), channelName) + } @@ -91,6 +93,15 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { FcmTopic.CAPSULE_SKIN.name -> { intent.putExtra("fragmentDestination", FragmentDestination.SKIN_FRAGMENT.name) } + + FcmTopic.FRIEND_REQUEST.name -> { + intent.putExtra("fragmentDestination", FragmentDestination.FRIEND_REQUEST_ACTIVITY.name) + } + + FcmTopic.FRIEND_ACCEPT.name -> { + intent.putExtra("fragmentDestination", FragmentDestination.FRIEND_ACCEPT_ACTIVITY.name) + } + else -> { } @@ -124,7 +135,9 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) - val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId).apply { + val notificationBuilder = NotificationCompat.Builder( + this@MyFirebaseMessagingService, channelId + ).apply { priority = NotificationCompat.PRIORITY_HIGH setSmallIcon(R.drawable.app_symbol) setContentTitle(remoteMessage.data["title"]) @@ -134,7 +147,11 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { setContentIntent(pendingIntent) bigPicture?.let { - setStyle(NotificationCompat.BigPictureStyle().bigPicture(it)) + setStyle(NotificationCompat.BigPictureStyle() + .bigPicture(it) + .bigLargeIcon(null as Bitmap?)) + + setLargeIcon(it) } } @@ -160,10 +177,15 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { } enum class FragmentDestination { - SKIN_FRAGMENT + HOME_FRAGMENT, + SKIN_FRAGMENT, + FRIEND_REQUEST_ACTIVITY, + FRIEND_ACCEPT_ACTIVITY, } enum class FcmTopic{ - CAPSULE_SKIN + CAPSULE_SKIN, + FRIEND_REQUEST, + FRIEND_ACCEPT, } } diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SocialLoginUtil.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SocialLoginUtil.kt index 0c0d491c6..6b7133f40 100644 --- a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SocialLoginUtil.kt +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SocialLoginUtil.kt @@ -91,6 +91,7 @@ class SocialLoginUtil(private val context: Context, private val callback: LoginC val authId = account.id.toString() val email = account.email ?: "" val profileUrl = account.photoUrl.toString() ?: "" + val token = account.idToken ?: "" callback.onLoginSuccess(CheckStatus(authId,AuthViewModel.Social.GOOGLE),SignUp(authId, email, profileUrl, AuthViewModel.Social.GOOGLE)) googleSignOut() } catch (e: ApiException) { diff --git a/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SpaceItemDecoration.kt b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SpaceItemDecoration.kt new file mode 100644 index 000000000..57b31361c --- /dev/null +++ b/frontend/ARchive/app/src/main/java/com/droidblossom/archive/util/SpaceItemDecoration.kt @@ -0,0 +1,19 @@ +package com.droidblossom.archive.util + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class SpaceItemDecoration( + private val spaceLeft: Int = 0, + private val spaceRight: Int = 0, + private val spaceTop: Int = 0, + private val spaceBottom: Int = 0 +) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + outRect.left = spaceLeft + outRect.right = spaceRight + outRect.top = spaceTop + outRect.bottom = spaceBottom + } +} \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/app_symbol_loading.xml b/frontend/ARchive/app/src/main/res/drawable/app_symbol_loading.xml new file mode 100644 index 000000000..4da037232 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/app_symbol_loading.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/corner_radius_12_stroke_1.xml b/frontend/ARchive/app/src/main/res/drawable/corner_radius_12_stroke_1.xml new file mode 100644 index 000000000..2f373efed --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/corner_radius_12_stroke_1.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_add_circle.xml b/frontend/ARchive/app/src/main/res/drawable/ic_add_circle.xml new file mode 100644 index 000000000..e2ef5e871 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_add_circle.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_add_person_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_add_person_24.xml new file mode 100644 index 000000000..494343f06 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_add_person_24.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_ar_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_ar_outline.xml new file mode 100644 index 000000000..868c54f63 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_ar_outline.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_arrow_up_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_arrow_up_24.xml new file mode 100644 index 000000000..7c85f9a5b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_arrow_up_24.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_call_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_call_24.xml new file mode 100644 index 000000000..f56f10fd4 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_call_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_camera_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_camera_outline.xml new file mode 100644 index 000000000..0f37a729f --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_camera_outline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_check_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_check_24.xml new file mode 100644 index 000000000..c203713f4 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_check_24.xml @@ -0,0 +1,18 @@ + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_cluster_marker.xml b/frontend/ARchive/app/src/main/res/drawable/ic_cluster_marker.xml new file mode 100644 index 000000000..5aa4a692e --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_cluster_marker.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_cluster_marker_46.xml b/frontend/ARchive/app/src/main/res/drawable/ic_cluster_marker_46.xml new file mode 100644 index 000000000..e0a91117b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_cluster_marker_46.xml @@ -0,0 +1,12 @@ + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_contact_phone_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_contact_phone_outline.xml new file mode 100644 index 000000000..71a115913 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_contact_phone_outline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_contacts_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_contacts_outline.xml new file mode 100644 index 000000000..28301fbdb --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_contacts_outline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_default_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_default_outline.xml new file mode 100644 index 000000000..6d629d60c --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_default_outline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_essential_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_essential_outline.xml new file mode 100644 index 000000000..812f72df3 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_essential_outline.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_left_arrow_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_left_arrow_24.xml new file mode 100644 index 000000000..0fc9dba20 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_left_arrow_24.xml @@ -0,0 +1,18 @@ + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_location_outline.xml b/frontend/ARchive/app/src/main/res/drawable/ic_location_outline.xml new file mode 100644 index 000000000..02425c36b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_location_outline.xml @@ -0,0 +1,14 @@ + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_menu_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_menu_24.xml new file mode 100644 index 000000000..015c411fa --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_menu_24.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_plus_small_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_plus_small_24.xml new file mode 100644 index 000000000..b90dd457e --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_plus_small_24.xml @@ -0,0 +1,18 @@ + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_refresh_24.xml b/frontend/ARchive/app/src/main/res/drawable/ic_refresh_24.xml new file mode 100644 index 000000000..98469ca33 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_refresh_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/ic_setting_24_main_1.xml b/frontend/ARchive/app/src/main/res/drawable/ic_setting_24_main_1.xml new file mode 100644 index 000000000..4c6c95000 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/ic_setting_24_main_1.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/ARchive/app/src/main/res/drawable/img.png b/frontend/ARchive/app/src/main/res/drawable/img.png new file mode 100644 index 000000000..e27dbdd48 Binary files /dev/null and b/frontend/ARchive/app/src/main/res/drawable/img.png differ diff --git a/frontend/ARchive/app/src/main/res/drawable/my_page_history_background.xml b/frontend/ARchive/app/src/main/res/drawable/my_page_history_background.xml new file mode 100644 index 000000000..f2235dfde --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/my_page_history_background.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/rectangle_solid_corner_10dp_stroke_1dp.xml b/frontend/ARchive/app/src/main/res/drawable/rectangle_solid_corner_10dp_stroke_1dp.xml new file mode 100644 index 000000000..fcaf1ed40 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/rectangle_solid_corner_10dp_stroke_1dp.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/spinner_background_close.xml b/frontend/ARchive/app/src/main/res/drawable/spinner_background_close.xml new file mode 100644 index 000000000..22140ee8e --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/spinner_background_close.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/spinner_background_open.xml b/frontend/ARchive/app/src/main/res/drawable/spinner_background_open.xml new file mode 100644 index 000000000..7d38881e4 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/spinner_background_open.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/spinner_dropdown_item_background.xml b/frontend/ARchive/app/src/main/res/drawable/spinner_dropdown_item_background.xml new file mode 100644 index 000000000..7ca49d672 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/spinner_dropdown_item_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/spinner_dropdown_item_background_last.xml b/frontend/ARchive/app/src/main/res/drawable/spinner_dropdown_item_background_last.xml new file mode 100644 index 000000000..da099ce46 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/spinner_dropdown_item_background_last.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/switch_thumb_off.xml b/frontend/ARchive/app/src/main/res/drawable/switch_thumb_off.xml new file mode 100644 index 000000000..3b02f332d --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/switch_thumb_off.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/switch_thumb_on.xml b/frontend/ARchive/app/src/main/res/drawable/switch_thumb_on.xml new file mode 100644 index 000000000..ec4d2b1de --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/switch_thumb_on.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/switch_thumb_selector.xml b/frontend/ARchive/app/src/main/res/drawable/switch_thumb_selector.xml new file mode 100644 index 000000000..9fcdcf1b2 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/switch_thumb_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/switch_track_off.xml b/frontend/ARchive/app/src/main/res/drawable/switch_track_off.xml new file mode 100644 index 000000000..d031ada31 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/switch_track_off.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/switch_track_on.xml b/frontend/ARchive/app/src/main/res/drawable/switch_track_on.xml new file mode 100644 index 000000000..1a423a206 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/switch_track_on.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/switch_track_selector.xml b/frontend/ARchive/app/src/main/res/drawable/switch_track_selector.xml new file mode 100644 index 000000000..ac079f050 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/switch_track_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/tab_background_selected.xml b/frontend/ARchive/app/src/main/res/drawable/tab_background_selected.xml new file mode 100644 index 000000000..dbbb4af20 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/tab_background_selected.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/tab_background_unselected.xml b/frontend/ARchive/app/src/main/res/drawable/tab_background_unselected.xml new file mode 100644 index 000000000..2212ced20 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/tab_background_unselected.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/drawable/tab_selector_background.xml b/frontend/ARchive/app/src/main/res/drawable/tab_selector_background.xml new file mode 100644 index 000000000..cdb552f97 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/drawable/tab_selector_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/activity_add_friend.xml b/frontend/ARchive/app/src/main/res/layout/activity_add_friend.xml new file mode 100644 index 000000000..cb7e29abf --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/activity_add_friend.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/activity_add_group.xml b/frontend/ARchive/app/src/main/res/layout/activity_add_group.xml new file mode 100644 index 000000000..5fabe4543 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/activity_add_group.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/activity_capsule_detail.xml b/frontend/ARchive/app/src/main/res/layout/activity_capsule_detail.xml index 39a25f9af..07600a942 100644 --- a/frontend/ARchive/app/src/main/res/layout/activity_capsule_detail.xml +++ b/frontend/ARchive/app/src/main/res/layout/activity_capsule_detail.xml @@ -52,33 +52,13 @@ android:layout_marginStart="16dp" app:civ_border_color="@color/white" app:civ_border_overlay="true" + android:layout_marginTop="36dp" app:civ_circle_background_color="@color/white" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/capsuleTypeCardView" + app:layout_constraintTop_toBottomOf="@id/closeBtn" bind:imageUrl="@{vm.capsuleDetail.profileUrl}" bind:placeholder="@{@drawable/app_symbol}" /> - - - - - - - + app:layout_constraintTop_toBottomOf="@id/userProfileImg"> + + + + + + + @@ -192,17 +199,27 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginEnd="42dp" android:ellipsize="end" android:lines="1" + android:layout_marginEnd="@dimen/margin" android:text="@{vm.capsuleDetail.nickname}" android:textAppearance="@style/TextAppearance.App.caption4" android:textColor="@color/gray_700" app:layout_constraintBottom_toBottomOf="@id/userProfileImg" - app:layout_constraintEnd_toStartOf="@id/capsuleTypeCardView" + app:layout_constraintEnd_toStartOf="@id/capsuleMenuImg" app:layout_constraintStart_toEndOf="@id/userProfileImg" app:layout_constraintTop_toTopOf="@id/userProfileImg" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/activity_friend_accept.xml b/frontend/ARchive/app/src/main/res/layout/activity_friend_accept.xml new file mode 100644 index 000000000..8e47469aa --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/activity_friend_accept.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/activity_notification.xml b/frontend/ARchive/app/src/main/res/layout/activity_notification.xml new file mode 100644 index 000000000..689a29df9 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/activity_notification.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/activity_setting.xml b/frontend/ARchive/app/src/main/res/layout/activity_setting.xml new file mode 100644 index 000000000..6030ab5d5 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/activity_setting.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/dialog_loading.xml b/frontend/ARchive/app/src/main/res/layout/dialog_loading.xml new file mode 100644 index 000000000..4eebd6534 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/dialog_loading.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/dialog_permission.xml b/frontend/ARchive/app/src/main/res/layout/dialog_permission.xml new file mode 100644 index 000000000..ec36206bf --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/dialog_permission.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_accept.xml b/frontend/ARchive/app/src/main/res/layout/fragment_accept.xml new file mode 100644 index 000000000..165621392 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_accept.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_add_group.xml b/frontend/ARchive/app/src/main/res/layout/fragment_add_group.xml new file mode 100644 index 000000000..5c297ecc6 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_add_group.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_camera.xml b/frontend/ARchive/app/src/main/res/layout/fragment_camera.xml index 34c9fd2c8..8bb024f46 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_camera.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_camera.xml @@ -1,12 +1,22 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + - @@ -16,5 +26,86 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_capsule_preview_dialog.xml b/frontend/ARchive/app/src/main/res/layout/fragment_capsule_preview_dialog.xml index cfc55228c..a1aa726aa 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_capsule_preview_dialog.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_capsule_preview_dialog.xml @@ -1,11 +1,13 @@ + xmlns:bind="http://schemas.android.com/tools" + xmlns:tools="http://schemas.android.com/tools"> - + + + @@ -14,9 +16,9 @@ + android:backgroundTint="@android:color/transparent"> @@ -53,33 +55,59 @@ android:id="@+id/capsuleTitleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@{vm.secretCapsuleSummary.title}" + android:ellipsize="middle" + android:maxLines="1" + android:text="@{vm.capsuleSummaryResponse.title}" android:textAppearance="@style/TextAppearance.App.h2" android:textColor="@color/white" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintStart_toEndOf="@id/capsuleTypeCardView" app:layout_constraintTop_toTopOf="parent" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/capsuleTitleTextView" + bind:displayCreationDateFormatted="@{vm.capsuleSummaryResponse.createdAt}" /> + bind:imageUrl="@{vm.capsuleSummaryResponse.profileUrl}" + bind:placeholder="@{@drawable/app_symbol}" /> - + android:ellipsize="end" + android:fontFamily="@font/suit_bold" + android:maxLines="1" + android:text="@{vm.capsuleSummaryResponse.nickname}" + android:textAppearance="@style/TextAppearance.App.caption4" + android:textColor="@color/gray_700" + app:layout_constraintBottom_toBottomOf="@id/profileImg" + app:layout_constraintEnd_toStartOf="@id/capsuleMenuImg" + app:layout_constraintStart_toEndOf="@id/profileImg" + app:layout_constraintTop_toTopOf="@id/profileImg" /> - + - - + app:strokeWidth="0dp"> + bind:url="@{vm.capsuleSummaryResponse.skinUrl}" /> - + + app:layout_constraintTop_toBottomOf="@id/profileImg" /> + app:layout_constraintTop_toBottomOf="@id/profileImg" /> + app:layout_constraintStart_toStartOf="@id/skinCardView" + app:layout_constraintTop_toTopOf="@id/skinCardView" /> @@ -221,9 +259,9 @@ android:layout_height="110dp" android:layout_marginHorizontal="16dp" android:layout_marginTop="8dp" - android:visibility="gone" android:background="@drawable/rectangle_solid_corner_30dp" android:backgroundTint="#ECEFFE" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/capsuleSkinLayout"> diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_common_dialog.xml b/frontend/ARchive/app/src/main/res/layout/fragment_common_dialog.xml new file mode 100644 index 000000000..a41d613bf --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_common_dialog.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_create_capsule2.xml b/frontend/ARchive/app/src/main/res/layout/fragment_create_capsule2.xml index f72f38134..2f685bb8f 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_create_capsule2.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_create_capsule2.xml @@ -32,7 +32,7 @@ diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_friend_search_nickname.xml b/frontend/ARchive/app/src/main/res/layout/fragment_friend_search_nickname.xml new file mode 100644 index 000000000..4fcd64662 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_friend_search_nickname.xml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_friend_search_number.xml b/frontend/ARchive/app/src/main/res/layout/fragment_friend_search_number.xml new file mode 100644 index 000000000..c00c1fc5b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_friend_search_number.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_home.xml b/frontend/ARchive/app/src/main/res/layout/fragment_home.xml index 1cadcd4cd..6f1ae4b09 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_home.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_home.xml @@ -27,38 +27,13 @@ - - - - - + app:layout_constraintTop_toTopOf="parent" /> + + diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_list_friend.xml b/frontend/ARchive/app/src/main/res/layout/fragment_list_friend.xml new file mode 100644 index 000000000..6cf4c1133 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_list_friend.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_list_group.xml b/frontend/ARchive/app/src/main/res/layout/fragment_list_group.xml new file mode 100644 index 000000000..1dbdeb15b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_list_group.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_my_page.xml b/frontend/ARchive/app/src/main/res/layout/fragment_my_page.xml index bbbcfefad..23beb1931 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_my_page.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_my_page.xml @@ -1,24 +1,18 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - - + android:background="@color/main_bg_1"> - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingHorizontal="16dp" + android:paddingBottom="70dp" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" /> - + - \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_setting_agree.xml b/frontend/ARchive/app/src/main/res/layout/fragment_setting_agree.xml new file mode 100644 index 000000000..35a5df475 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_setting_agree.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_setting_main.xml b/frontend/ARchive/app/src/main/res/layout/fragment_setting_main.xml new file mode 100644 index 000000000..2d0ba93c8 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_setting_main.xml @@ -0,0 +1,423 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_setting_notice.xml b/frontend/ARchive/app/src/main/res/layout/fragment_setting_notice.xml new file mode 100644 index 000000000..ea0a75a3e --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_setting_notice.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_setting_notification.xml b/frontend/ARchive/app/src/main/res/layout/fragment_setting_notification.xml new file mode 100644 index 000000000..a98138e3b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_setting_notification.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_setting_user.xml b/frontend/ARchive/app/src/main/res/layout/fragment_setting_user.xml new file mode 100644 index 000000000..d87fa539f --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_setting_user.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_sign_in.xml b/frontend/ARchive/app/src/main/res/layout/fragment_sign_in.xml index 1d587fb9b..bba54f643 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_sign_in.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_sign_in.xml @@ -76,9 +76,9 @@ app:layout_constraintBottom_toTopOf="@id/googleLoginBtn" android:text="@string/kakaoLogin" android:textAppearance="@style/TextAppearance.App.button" - android:textColor="@color/white" + android:textColor="@color/black" android:background="@drawable/rectangle_solid_corner_8dp" - android:backgroundTint="@color/main_1" + android:backgroundTint="@color/kakao" android:layout_marginBottom="@dimen/margin_small" android:layout_marginHorizontal="@dimen/margin"/> diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_skin.xml b/frontend/ARchive/app/src/main/res/layout/fragment_skin.xml index bfe6afda5..c4c5f4265 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_skin.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_skin.xml @@ -38,7 +38,7 @@ android:visibility="@{vm.isSearchOpen ? View.VISIBLE : View.GONE}" android:id="@+id/searchOpenBtn" android:layout_width="0dp" - android:layout_height="44dp" + android:layout_height="36dp" app:layout_constraintStart_toStartOf="parent" app:cardBackgroundColor="@color/white" app:cardCornerRadius="22dp" @@ -93,7 +93,7 @@ android:visibility="@{vm.isSearchOpen ? View.GONE : View.VISIBLE}" android:id="@+id/searchBtn" android:layout_width="58dp" - android:layout_height="34dp" + android:layout_height="36dp" android:onClick="@{()->vm.openSearchSkin()}" app:cardBackgroundColor="@color/white" app:cardCornerRadius="17dp" @@ -127,30 +127,36 @@ android:id="@+id/skinRV" android:layout_width="0dp" android:layout_height="0dp" + android:layout_marginTop="100dp" + android:elevation="1dp" android:orientation="vertical" + android:clipToPadding="false" + android:paddingBottom="70dp" android:visibility="visible" - android:layout_marginTop="100dp" - app:layout_constraintBottom_toTopOf="@id/place" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" - app:spanCount="3" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/viewHeaderTitle" /> + app:layout_constraintTop_toBottomOf="@id/viewHeaderTitle" + app:layout_constraintVertical_bias="1.0" + app:spanCount="3" /> + app:cardElevation="1dp" + android:layout_marginBottom="110dp"> @@ -193,14 +199,6 @@ - - diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_skin_make.xml b/frontend/ARchive/app/src/main/res/layout/fragment_skin_make.xml index 203328717..bb5d8beb8 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_skin_make.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_skin_make.xml @@ -62,6 +62,8 @@ android:hint="@string/skinMakeHint" android:text="@={vm.skinName}" android:maxLines="1" + android:inputType="text" + android:imeOptions="actionDone" android:textAppearance="@style/TextAppearance.App.h2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -134,7 +136,7 @@ + android:layout_height="match_parent" + android:paddingVertical="20dp"> @@ -167,26 +168,29 @@ android:id="@+id/imagePlusBtn" android:layout_width="24dp" android:layout_height="24dp" - android:layout_marginEnd="18dp" android:background="@drawable/corner_radius_12" android:backgroundTint="@color/main_2" android:tint="@color/white" + android:layout_marginEnd="20dp" bind:minus_plus="@{vm.addMotion}" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="@id/addMotionTextView" /> + app:layout_constraintTop_toBottomOf="@id/addMotionTextView" + app:spanCount="3"/> diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_skin_make_success.xml b/frontend/ARchive/app/src/main/res/layout/fragment_skin_make_success.xml index 0e9757c0d..9f220d2d1 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_skin_make_success.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_skin_make_success.xml @@ -66,8 +66,9 @@ android:layout_marginTop="32dp" android:textAppearance="@style/TextAppearance.App.h4" android:textColor="@color/gray_600" + android:gravity="center" android:visibility="@{vm.addMotion ? View.VISIBLE : View.GONE}" - android:text="모션이 적용되면 알림이 울려요" + android:text="스킨을 생성하는 데 약 50초가 소요됩니다.\n 모션이 적용되면 알림을 보내드릴게요!" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/skinMakeMessage" /> diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_skin_preview_dialog.xml b/frontend/ARchive/app/src/main/res/layout/fragment_skin_preview_dialog.xml new file mode 100644 index 000000000..67ed5d4c8 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_skin_preview_dialog.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_social.xml b/frontend/ARchive/app/src/main/res/layout/fragment_social.xml index c74a82000..c2af9b3d6 100644 --- a/frontend/ARchive/app/src/main/res/layout/fragment_social.xml +++ b/frontend/ARchive/app/src/main/res/layout/fragment_social.xml @@ -1,16 +1,70 @@ - + - + + + + + android:background="@color/main_bg_1" + tools:context=".presentation.ui.social.SocialFragment"> + + + + + + + + + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_social_friend.xml b/frontend/ARchive/app/src/main/res/layout/fragment_social_friend.xml new file mode 100644 index 000000000..1228f5f31 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_social_friend.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/fragment_social_group.xml b/frontend/ARchive/app/src/main/res/layout/fragment_social_group.xml new file mode 100644 index 000000000..3d81248c8 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/fragment_social_group.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_accept.xml b/frontend/ARchive/app/src/main/res/layout/item_accept.xml new file mode 100644 index 000000000..3340d40cd --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_accept.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_add_friend.xml b/frontend/ARchive/app/src/main/res/layout/item_add_friend.xml new file mode 100644 index 000000000..4c899ac4b --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_add_friend.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_agree.xml b/frontend/ARchive/app/src/main/res/layout/item_agree.xml new file mode 100644 index 000000000..08927fed7 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_agree.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_capsule_type.xml b/frontend/ARchive/app/src/main/res/layout/item_capsule_type.xml new file mode 100644 index 000000000..046666470 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_capsule_type.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_friend.xml b/frontend/ARchive/app/src/main/res/layout/item_friend.xml new file mode 100644 index 000000000..0a75b60dd --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_friend.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_my_page_capsule.xml b/frontend/ARchive/app/src/main/res/layout/item_my_page_capsule.xml new file mode 100644 index 000000000..a2b3e7ccf --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_my_page_capsule.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_my_page_profile.xml b/frontend/ARchive/app/src/main/res/layout/item_my_page_profile.xml new file mode 100644 index 000000000..0224f6169 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_my_page_profile.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_my_page_spinner.xml b/frontend/ARchive/app/src/main/res/layout/item_my_page_spinner.xml new file mode 100644 index 000000000..318b359aa --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_my_page_spinner.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_my_page_story.xml b/frontend/ARchive/app/src/main/res/layout/item_my_page_story.xml new file mode 100644 index 000000000..20829247c --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_my_page_story.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_my_skin.xml b/frontend/ARchive/app/src/main/res/layout/item_my_skin.xml index 4cfd88dbd..d2aa11f3b 100644 --- a/frontend/ARchive/app/src/main/res/layout/item_my_skin.xml +++ b/frontend/ARchive/app/src/main/res/layout/item_my_skin.xml @@ -16,20 +16,24 @@ android:layout_height="wrap_content" android:layout_margin="4dp" app:cardCornerRadius="16dp" - app:cardElevation="4dp"> + app:strokeColor="@color/white" + android:background="@color/white" + android:elevation="0dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_notice_content.xml b/frontend/ARchive/app/src/main/res/layout/item_notice_content.xml new file mode 100644 index 000000000..44d45e0b7 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_notice_content.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_notification.xml b/frontend/ARchive/app/src/main/res/layout/item_notification.xml new file mode 100644 index 000000000..66b9135ae --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_notification.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_skin_motion.xml b/frontend/ARchive/app/src/main/res/layout/item_skin_motion.xml new file mode 100644 index 000000000..32f5f13c5 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_skin_motion.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_social_capsule_close.xml b/frontend/ARchive/app/src/main/res/layout/item_social_capsule_close.xml new file mode 100644 index 000000000..f9fb0a0fd --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_social_capsule_close.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_social_capsule_open.xml b/frontend/ARchive/app/src/main/res/layout/item_social_capsule_open.xml new file mode 100644 index 000000000..559f01a06 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_social_capsule_open.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_spinner.xml b/frontend/ARchive/app/src/main/res/layout/item_spinner.xml new file mode 100644 index 000000000..e84abcc6f --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_spinner.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_spinner_dropdown.xml b/frontend/ARchive/app/src/main/res/layout/item_spinner_dropdown.xml new file mode 100644 index 000000000..f758ab93a --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/item_spinner_dropdown.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/item_story.xml b/frontend/ARchive/app/src/main/res/layout/item_story.xml index e8728e453..2a0ecab25 100644 --- a/frontend/ARchive/app/src/main/res/layout/item_story.xml +++ b/frontend/ARchive/app/src/main/res/layout/item_story.xml @@ -1,17 +1,18 @@ - + android:src="@drawable/ic_plus_main_40" /> - + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/cardView" + android:layout_marginTop="8dp"/> \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/layout/popup_menu_capsule.xml b/frontend/ARchive/app/src/main/res/layout/popup_menu_capsule.xml new file mode 100644 index 000000000..a9d6f0257 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/layout/popup_menu_capsule.xml @@ -0,0 +1,37 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/navigation/nav_add_friend_graph.xml b/frontend/ARchive/app/src/main/res/navigation/nav_add_friend_graph.xml new file mode 100644 index 000000000..f4360cc67 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/navigation/nav_add_friend_graph.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/navigation/nav_setting_graph.xml b/frontend/ARchive/app/src/main/res/navigation/nav_setting_graph.xml new file mode 100644 index 000000000..a12b44105 --- /dev/null +++ b/frontend/ARchive/app/src/main/res/navigation/nav_setting_graph.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/values/colors.xml b/frontend/ARchive/app/src/main/res/values/colors.xml index 851fd6ebc..6f4940ea0 100644 --- a/frontend/ARchive/app/src/main/res/values/colors.xml +++ b/frontend/ARchive/app/src/main/res/values/colors.xml @@ -20,12 +20,19 @@ #FF424242 #FF2E2E2E #FF1C1C1C + #FEE500 #324263F7 #801B3AC4 + #E6FFFFFF + #CCFFFFFF #B3FFFFFF #99FFFFFF + #80FFFFFF + #66FFFFFF + #4DFFFFFF + #33FFFFFF #D94263F7 #B3ECEFFE #B31B3AC4 diff --git a/frontend/ARchive/app/src/main/res/values/dimen.xml b/frontend/ARchive/app/src/main/res/values/dimen.xml index a236ba5bb..c1d0fcb57 100644 --- a/frontend/ARchive/app/src/main/res/values/dimen.xml +++ b/frontend/ARchive/app/src/main/res/values/dimen.xml @@ -24,4 +24,7 @@ 104dp 60dp + + 300dp + 50dp \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/values/strings.xml b/frontend/ARchive/app/src/main/res/values/strings.xml index b244f3cc3..f8d0da4bf 100644 --- a/frontend/ARchive/app/src/main/res/values/strings.xml +++ b/frontend/ARchive/app/src/main/res/values/strings.xml @@ -30,4 +30,30 @@ 나만의 스킨이 탄생했어요! 나만의 스킨이 곧 탄생해요! + +  개인정보처리자는 다음의 어느 하나에 해당하는 경우에는 개인정보를 수집할 수 있으며 그 수집 목적의 범위에서 이용할 수 있습니다(「개인정보 보호법」 제15조제1항). + 정보주체의 동의를 받은 경우 + 법률에 특별한 규정이 있거나 법령상 의무를 준수하기 위하여 불가피한 경우 + 공공기관이 법령 등에서 정하는 소관 업무의 수행을 위하여 불가피한 경우 + 정보주체와의 계약의 체결한 계약을 이행하거나 계약을 체결하는 과정에서 정보주체의 요청에 따른 조치를 이행하기 위해 필요한 경우 + 명백히 정보주체 또는 제3자의 급박한 생명, 신체, 재산의 이익을 위하여 필요하다고 인정되는 경우 + 개인정보처리자의 정당한 이익을 달성하기 위하여 필요한 경우로서 명백하게 정보주체의 권리보다 우선하는 경우. 이 경우 개인정보처리자의 정당한 이익과 상당한 관련이 있고 합리적인 범위를 초과하지 않은 경우에 한함 + 공중위생 등 공공의 안전과 안녕을 위해 긴급히 필요한 경우 +※ 이를 위반하여 개인정보를 수집한 자는 개인정보 보호위원회에게 전체 매출액의 100분의 3을 초과하지 않는 범위에서 과징금을 부과 받을 수 있습니다(「개인정보 보호법」 제64조의2제1항제1호). + + 커뮤니티 + 그룹 + 친구 + + 그룹 리스트 + 친구 리스트 + + 그룹 요청 + 친구 요청 + + + Secret + Public + Group + \ No newline at end of file diff --git a/frontend/ARchive/app/src/main/res/values/style.xml b/frontend/ARchive/app/src/main/res/values/style.xml index 811b5a8c5..c00de7544 100644 --- a/frontend/ARchive/app/src/main/res/values/style.xml +++ b/frontend/ARchive/app/src/main/res/values/style.xml @@ -14,7 +14,7 @@ + + + + + \ No newline at end of file diff --git a/frontend/ARchive/build.gradle b/frontend/ARchive/build.gradle index 641cb29ab..b2ad3fa8f 100644 --- a/frontend/ARchive/build.gradle +++ b/frontend/ARchive/build.gradle @@ -1,6 +1,7 @@ buildscript { dependencies { classpath 'com.google.gms:google-services:4.3.14' + classpath('com.google.android.gms:oss-licenses-plugin:0.10.4') } }// Top-level build file where you can add configuration options common to all sub-projects/modules. diff --git a/frontend/ARchive/settings.gradle b/frontend/ARchive/settings.gradle index 82fe46e22..231b9bf9b 100644 --- a/frontend/ARchive/settings.gradle +++ b/frontend/ARchive/settings.gradle @@ -13,7 +13,7 @@ dependencyResolutionManagement { maven { url "https://jitpack.io" } maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/' } // 카카오 로그인 maven { - url 'https://naver.jfrog.io/artifactory/maven/' + url 'https://repository.map.naver.com/archive/maven' } } }