diff --git a/README.md b/README.md index 52000b23..8935c478 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,21 @@ --- -## ๐Ÿ“ ๊ฐœ๋ฐœ ์‹œ์Šคํ…œ ๊ตฌ์กฐ๋„ +## ๐Ÿ“ ๊ฐœ๋ฐœ ์‹œ์Šคํ…œ ๊ตฌ์„ฑ๋„ ![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/5df840e4-9c04-4d01-8fe1-119f53d4ae12) +## ๐Ÿ“™ ์‹œ์Šคํ…œ ๋””์ž์ธ +![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/62e40a3d-8143-4caf-aa55-1a73f7d97e90) + +![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/9694ef38-8112-4448-885c-9a0f49886eb5) | ![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/7cb02e75-bf3c-4387-bd5c-44d012575c9e) +---|---| + +![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/cece44e4-8a78-4b5b-877f-dc43597d4ecc) | ![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/af4b50a7-33e8-4563-ac8b-462c87a3b380) +---|---| + +![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/5cb15e5e-4f14-4f62-aad6-93d3de38305c) +---| + ## ๐Ÿ”— ERD ![](https://github.com/tukcomCD2024/JinJiHan/assets/81320703/99ab2be8-b7a0-417e-8e94-6ec2f3f6b118) diff --git a/backend/core/build.gradle b/backend/core/build.gradle index 71dc2d2f..4d2ceb6e 100644 --- a/backend/core/build.gradle +++ b/backend/core/build.gradle @@ -22,6 +22,9 @@ repositories { } dependencies { + if (isAppleSilicon()) { + runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64") + } implementation 'com.auth0:java-jwt:4.2.1' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' @@ -37,6 +40,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.15.3' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-webflux' annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' @@ -52,3 +56,7 @@ tasks.named('bootBuildImage') { tasks.named('test') { useJUnitPlatform() } + +def boolean isAppleSilicon() { + return System.getProperty("os.name") == "Mac OS X" && System.getProperty("os.arch") == "aarch64" +} \ No newline at end of file diff --git a/backend/core/src/main/java/com/rollthedice/backend/BackendApplication.java b/backend/core/src/main/java/com/rollthedice/backend/BackendApplication.java index 13a91fbb..4364e06a 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/BackendApplication.java +++ b/backend/core/src/main/java/com/rollthedice/backend/BackendApplication.java @@ -5,7 +5,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; -@EnableJpaAuditing @EnableScheduling @SpringBootApplication public class BackendApplication { diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java index df91954e..83d85e7e 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/bookmark/service/BookmarkService.java @@ -3,12 +3,11 @@ import com.rollthedice.backend.domain.bookmark.entity.Bookmark; import com.rollthedice.backend.domain.bookmark.repository.BookmarkRepository; import com.rollthedice.backend.domain.member.entity.Member; -import com.rollthedice.backend.domain.member.query.AuthService; +import com.rollthedice.backend.global.oauth2.service.AuthService; import com.rollthedice.backend.domain.news.dto.response.NewsResponse; import com.rollthedice.backend.domain.news.entity.News; import com.rollthedice.backend.domain.news.mapper.NewsMapper; import com.rollthedice.backend.domain.news.repository.NewsRepository; -import com.rollthedice.backend.domain.news.service.NewsService; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateRoomService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateRoomService.java index 5a18f9a6..ba084130 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateRoomService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/debate/service/DebateRoomService.java @@ -5,7 +5,7 @@ import com.rollthedice.backend.domain.debate.mapper.DebateRoomMapper; import com.rollthedice.backend.domain.debate.repository.DebateRoomRepository; import com.rollthedice.backend.domain.member.entity.Member; -import com.rollthedice.backend.domain.member.query.AuthService; +import com.rollthedice.backend.global.oauth2.service.AuthService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/LoginController.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/LoginController.java deleted file mode 100644 index 219f3c68..00000000 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/LoginController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.rollthedice.backend.domain.member.controller; - -import com.rollthedice.backend.domain.member.dto.SignUpDto; -import com.rollthedice.backend.domain.member.service.MemberService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -public class LoginController { - private final MemberService memberService; - - @ResponseStatus(HttpStatus.OK) - @PostMapping("/oauth2/sign-up") - public void signUp(@RequestBody SignUpDto dto) { - memberService.signUp(dto); - } - -} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/MemberController.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/MemberController.java index be1f8857..4817d16d 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/MemberController.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/controller/MemberController.java @@ -1,13 +1,14 @@ package com.rollthedice.backend.domain.member.controller; +import com.rollthedice.backend.domain.member.dto.MemberServiceDto; +import com.rollthedice.backend.domain.member.dto.MemberUpdateDto; import com.rollthedice.backend.domain.member.dto.response.MemberResponse; import com.rollthedice.backend.domain.member.service.MemberService; +import com.rollthedice.backend.global.annotation.LoginMemberEmail; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -15,6 +16,19 @@ public class MemberController { private final MemberService memberService; + @PostMapping + public ResponseEntity updateMember(@LoginMemberEmail String email, + @RequestBody MemberUpdateDto memberUpdateDto) { + MemberServiceDto memberServiceDto = memberUpdateDto.toServiceDto(email); + + if (memberService.isDuplicatedNickname(memberServiceDto)) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + memberService.update(memberServiceDto); + + return ResponseEntity.status(HttpStatus.OK).build(); + } + @ResponseStatus(HttpStatus.OK) @GetMapping("") public MemberResponse getMemberInfo() { diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/dto/MemberServiceDto.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/dto/MemberServiceDto.java new file mode 100644 index 00000000..c61910fd --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/dto/MemberServiceDto.java @@ -0,0 +1,14 @@ +package com.rollthedice.backend.domain.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class MemberServiceDto { + private String email; + private String nickname; + private String imageUrl; +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/dto/MemberUpdateDto.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/dto/MemberUpdateDto.java new file mode 100644 index 00000000..8e0cd929 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/dto/MemberUpdateDto.java @@ -0,0 +1,26 @@ +package com.rollthedice.backend.domain.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@Schema(description = "๋ฉค๋ฒ„ ์—…๋ฐ์ดํŠธ ํฌ๋งท") +public class MemberUpdateDto { + @Schema(description = "๋ณ€๊ฒฝํ•  ๋‹‰๋„ค์ž„") + private String nickname; + @Schema(description = "๋ณ€๊ฒฝํ•  ์ด๋ฏธ์ง€ S3 Url") + private String imageUrl; + + public MemberServiceDto toServiceDto(String email) { + return MemberServiceDto.builder() + .email(email) + .nickname(this.nickname) + .imageUrl(this.imageUrl) + .build(); + } +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/Member.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/Member.java index 81c0d3e1..6395057d 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/Member.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/Member.java @@ -1,5 +1,6 @@ package com.rollthedice.backend.domain.member.entity; +import com.rollthedice.backend.domain.member.dto.MemberServiceDto; import com.rollthedice.backend.global.config.BaseTimeEntity; import jakarta.persistence.*; import lombok.*; @@ -29,10 +30,10 @@ public class Member extends BaseTimeEntity { @Enumerated(EnumType.STRING) private Status status; - public Member update(String email, String imageUrl) { - this.email = email; - this.imageUrl = imageUrl; - return this; + public void update(MemberServiceDto dto) { + this.email = dto.getEmail(); + this.imageUrl = dto.getImageUrl(); + this.nickname = dto.getNickname(); } public void signUp(String nickname) { diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/SocialType.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/SocialType.java index 5095a4ff..abd61227 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/SocialType.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/entity/SocialType.java @@ -1,6 +1,20 @@ package com.rollthedice.backend.domain.member.entity; +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter public enum SocialType { - APPLE, - KAKAO + APPLE("https://"), + KAKAO("https://kapi.kakao.com/v2/user/me"); + + + private final String providerUrl; + + @JsonCreator + public static SocialType from(String s) { + return SocialType.valueOf(s.toUpperCase()); + } } diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/query/AuthService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/query/AuthService.java deleted file mode 100644 index 9b7a8662..00000000 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/query/AuthService.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.rollthedice.backend.domain.member.query; - -import com.rollthedice.backend.domain.member.entity.Member; -import com.rollthedice.backend.domain.member.repository.MemberRepository; -import com.rollthedice.backend.global.jwt.service.JwtService; -import com.rollthedice.backend.global.query.QueryService; -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; - -@Slf4j -@QueryService -@RequiredArgsConstructor -public class AuthService { - private final MemberRepository memberRepository; - private final JwtService jwtService; - - public Long getMemberId() { - return getMember().getId(); - } - - public Member getMember() { - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - return memberRepository.findByEmail(userDetails.getUsername()).orElseThrow(EntityNotFoundException::new); - } - -} diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/repository/MemberRepository.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/repository/MemberRepository.java index 0f504f18..0e28fa7e 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/repository/MemberRepository.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/repository/MemberRepository.java @@ -12,4 +12,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); Optional findBySocialTypeAndOauthId(SocialType socialType, String oauthId); + + boolean existsMemberByNickname(String nickname); } diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java index 3150a4f8..9e88af51 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/member/service/MemberService.java @@ -1,11 +1,14 @@ package com.rollthedice.backend.domain.member.service; +import com.rollthedice.backend.domain.member.dto.MemberServiceDto; import com.rollthedice.backend.domain.member.dto.SignUpDto; import com.rollthedice.backend.domain.member.dto.response.MemberResponse; import com.rollthedice.backend.domain.member.entity.Member; -import com.rollthedice.backend.domain.member.query.AuthService; -import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService; -import com.rollthedice.backend.global.jwt.service.JwtService; +import com.rollthedice.backend.domain.member.repository.MemberRepository; +import com.rollthedice.backend.global.oauth2.service.AuthService; +import com.rollthedice.backend.global.security.jwt.refresh.service.RefreshTokenService; +import com.rollthedice.backend.global.security.jwt.service.JwtService; +import jakarta.persistence.EntityNotFoundException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -20,15 +23,22 @@ public class MemberService { private final JwtService jwtService; private final HttpServletRequest request; private final HttpServletResponse response; + private final MemberRepository memberRepository; + + + @Transactional(readOnly = true) + public boolean isDuplicatedNickname(MemberServiceDto memberServiceDto) { + return memberRepository.existsMemberByNickname(memberServiceDto.getNickname()); + } @Transactional - public void signUp(SignUpDto dto) { - Member member = authService.getMember(); - member.signUp(dto.getNickname()); + public void update(MemberServiceDto memberServiceDto) { + findByEmail(memberServiceDto.getEmail()).update(memberServiceDto); + } - String refreshToken = jwtService.createRefreshToken(); - jwtService.setRefreshTokenHeader(response, refreshToken); - refreshTokenService.updateToken(member.getEmail(), refreshToken); + @Transactional(readOnly = true) + public Member findByEmail(String email) { + return memberRepository.findByEmail(email).orElseThrow(EntityNotFoundException::new); } public MemberResponse getMemberInfo() { diff --git a/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java b/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java index cb59ca9a..3ab16298 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/domain/news/service/NewsService.java @@ -2,7 +2,7 @@ import com.rollthedice.backend.domain.bookmark.service.BookmarkService; import com.rollthedice.backend.domain.member.entity.Member; -import com.rollthedice.backend.domain.member.query.AuthService; +import com.rollthedice.backend.global.oauth2.service.AuthService; import com.rollthedice.backend.domain.news.contentqueue.ContentProducer; import com.rollthedice.backend.domain.news.dto.ContentMessageDto; import com.rollthedice.backend.domain.news.dto.NewsUrlDto; diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/advice/ExceptionAdvice.java b/backend/core/src/main/java/com/rollthedice/backend/global/advice/ExceptionAdvice.java index 9ae7bf53..bd265554 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/advice/ExceptionAdvice.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/advice/ExceptionAdvice.java @@ -1,6 +1,6 @@ package com.rollthedice.backend.global.advice; -import com.rollthedice.backend.global.jwt.exception.NotFoundTokenException; +import com.rollthedice.backend.global.security.jwt.exception.NotFoundTokenException; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/annotation/LoginMemberEmail.java b/backend/core/src/main/java/com/rollthedice/backend/global/annotation/LoginMemberEmail.java new file mode 100644 index 00000000..859af590 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/annotation/LoginMemberEmail.java @@ -0,0 +1,11 @@ +package com.rollthedice.backend.global.annotation; + +import java.lang.annotation.*; + + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LoginMemberEmail { +} + diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/config/JpaAuditingConfig.java b/backend/core/src/main/java/com/rollthedice/backend/global/config/JpaAuditingConfig.java new file mode 100644 index 00000000..73972576 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package com.rollthedice.backend.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/config/SecurityConfig.java b/backend/core/src/main/java/com/rollthedice/backend/global/config/SecurityConfig.java index 8ab6b4ac..6b532daf 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/config/SecurityConfig.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/config/SecurityConfig.java @@ -1,19 +1,15 @@ package com.rollthedice.backend.global.config; import com.rollthedice.backend.domain.member.repository.MemberRepository; -import com.rollthedice.backend.global.jwt.filter.JwtAuthenticationProcessingFilter; -import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService; -import com.rollthedice.backend.global.jwt.service.JwtService; -import com.rollthedice.backend.global.oauth2.handler.OAuth2LoginFailureHandler; -import com.rollthedice.backend.global.oauth2.handler.OAuth2LoginSuccessHandler; -import com.rollthedice.backend.global.oauth2.service.CustomOAuth2UserService; +import com.rollthedice.backend.global.security.jwt.filter.JwtAuthenticationProcessingFilter; +import com.rollthedice.backend.global.security.jwt.refresh.service.RefreshTokenService; +import com.rollthedice.backend.global.security.jwt.service.JwtService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @@ -24,34 +20,26 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { + private final JwtService jwtService; private final MemberRepository memberRepository; - private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; - private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler; - private final CustomOAuth2UserService customOAuth2UserService; private final RefreshTokenService refreshTokenService; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .formLogin(AbstractHttpConfigurer::disable) //FormLogin ์‚ฌ์šฉ X - .httpBasic(AbstractHttpConfigurer::disable) //httpBasic ์‚ฌ์šฉ X + .formLogin(AbstractHttpConfigurer::disable) // ๊ธฐ๋ณธ ์ œ๊ณตํ•˜๋Š” ๋กœ๊ทธ์ธ Form ์‚ฌ์šฉ X + .httpBasic(AbstractHttpConfigurer::disable) // Bearer๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— httpBasic ์‚ฌ์šฉ X .csrf(AbstractHttpConfigurer::disable) //csrf ๋ณด์•ˆ ์‚ฌ์šฉ X - .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable).disable()) - .sessionManagement((sessionManagement) -> + .sessionManagement((sessionManagement) -> // ์„ธ์…˜์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— stateless ์„ค์ • sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(authorizeRequests -> authorizeRequests - .anyRequest().permitAll()) - .oauth2Login(oauth2 -> oauth2 - .loginPage("/login") - .successHandler(oAuth2LoginSuccessHandler) - .failureHandler(oAuth2LoginFailureHandler) - .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) //customUserService ์„ค์ • - ); + .anyRequest().permitAll()); + http.addFilterAfter(jwtAuthenticationProcessingFilter(), LogoutFilter.class); + return http.build(); } @@ -62,6 +50,6 @@ public PasswordEncoder passwordEncoder() { @Bean public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() { - return new JwtAuthenticationProcessingFilter(jwtService, refreshTokenService, memberRepository); + return new JwtAuthenticationProcessingFilter(jwtService, memberRepository, refreshTokenService); } } diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/controller/AuthController.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/controller/AuthController.java new file mode 100644 index 00000000..18a20501 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/controller/AuthController.java @@ -0,0 +1,23 @@ +package com.rollthedice.backend.global.oauth2.controller; + +import com.rollthedice.backend.global.oauth2.dto.LoginRequest; +import com.rollthedice.backend.global.oauth2.service.AuthService; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request, HttpServletResponse response) { + authService.authenticateOrRegisterUser(request, response); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/dto/LoginRequest.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/dto/LoginRequest.java new file mode 100644 index 00000000..0dbf8e09 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/dto/LoginRequest.java @@ -0,0 +1,17 @@ +package com.rollthedice.backend.global.oauth2.dto; + +import com.rollthedice.backend.domain.member.entity.SocialType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(description = "๋กœ๊ทธ์ธ ํฌ๋งท") +public class LoginRequest { + @Schema(description = "์ธ์ฆ์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜จ access token์„ ์ž…๋ ฅ") + private String token; + @Schema(description = "์ธ์ฆ์„œ๋ฒ„ํƒ€์ž…, APPLE, KAKAO ๊ฐ€๋Šฅ") + private SocialType socialType; +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/handler/OAuth2LoginFailureHandler.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/handler/OAuth2LoginFailureHandler.java deleted file mode 100644 index 1c759eda..00000000 --- a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/handler/OAuth2LoginFailureHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.rollthedice.backend.global.oauth2.handler; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Slf4j -@Component -public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler { - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.getWriter().write("์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค."); - log.info("์†Œ์…œ ๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ : {}", exception.getMessage()); - } -} \ No newline at end of file diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/handler/OAuth2LoginSuccessHandler.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/handler/OAuth2LoginSuccessHandler.java deleted file mode 100644 index 4dc8696f..00000000 --- a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/handler/OAuth2LoginSuccessHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.rollthedice.backend.global.oauth2.handler; - -import com.rollthedice.backend.domain.member.entity.Role; -import com.rollthedice.backend.global.jwt.service.JwtService; -import com.rollthedice.backend.global.oauth2.CustomOAuth2User; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Slf4j -@Component -@RequiredArgsConstructor -public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { - private final JwtService jwtService; - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - log.info("OAuth2 Login succeed."); - CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); - - if(oAuth2User.getRole() == Role.GUEST) { - String accessToken = jwtService.createAccessToken(oAuth2User.getEmail()); - response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken); - response.sendRedirect("oauth2/sign-up"); - - jwtService.sendAccessAndRefreshToken(response, accessToken, null); - } else { - loginSuccess(response, oAuth2User); - response.sendRedirect("/"); - } - } - - private void loginSuccess(HttpServletResponse response, CustomOAuth2User oAuth2User) throws IOException { - log.info("Role == User => refresh token ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."); - - String accessToken = jwtService.createAccessToken(oAuth2User.getEmail()); - String refreshToken = jwtService.createRefreshToken(); - response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken); - response.addHeader(jwtService.getRefreshHeader(), "Bearer " + refreshToken); - - jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken); - jwtService.updateRefreshToken(oAuth2User.getEmail(), refreshToken); - } -} - diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java new file mode 100644 index 00000000..3dd4beb4 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/AuthService.java @@ -0,0 +1,60 @@ +package com.rollthedice.backend.global.oauth2.service; + +import com.rollthedice.backend.domain.member.entity.Member; +import com.rollthedice.backend.domain.member.entity.Role; +import com.rollthedice.backend.domain.member.entity.SocialType; +import com.rollthedice.backend.domain.member.repository.MemberRepository; +import com.rollthedice.backend.global.oauth2.dto.LoginRequest; +import com.rollthedice.backend.global.oauth2.userInfo.OAuth2UserInfo; +import com.rollthedice.backend.global.security.jwt.service.JwtService; +import com.rollthedice.backend.global.query.QueryService; +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.UUID; + +@Slf4j +@QueryService +@RequiredArgsConstructor +public class AuthService { + private final MemberRepository memberRepository; + private final OAuth2ProviderService oAuth2ProviderService; + private final JwtService jwtService; + + public void authenticateOrRegisterUser(LoginRequest loginRequest, HttpServletResponse response) { + OAuth2UserInfo userInfo = oAuth2ProviderService.getUserInfo(loginRequest); + Member member = findOrElseRegisterMember(userInfo, loginRequest.getSocialType()); + jwtService.sendAccessAndRefreshToken(response, member.getEmail()); + } + + private Member findOrElseRegisterMember(OAuth2UserInfo userInfo, SocialType socialType) { + return memberRepository.findBySocialTypeAndOauthId(socialType, userInfo.getId()) + .orElse(registerMember(socialType, userInfo)); + } + + private Member registerMember(SocialType socialType, OAuth2UserInfo userInfo) { + Member member = Member.builder() + .socialType(socialType) + .oauthId(userInfo.getId()) + .email(UUID.randomUUID() + "@socialUser.com") + .nickname(String.valueOf(UUID.randomUUID())) + .imageUrl(userInfo.getImageUrl()) + .role(Role.USER) + .build(); + + return memberRepository.save(member); + } + + public Member getMember() { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return memberRepository.findByEmail(userDetails.getUsername()).orElseThrow(EntityNotFoundException::new); + } + +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/CustomOAuth2UserService.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/CustomOAuth2UserService.java deleted file mode 100644 index f26741b1..00000000 --- a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/CustomOAuth2UserService.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.rollthedice.backend.global.oauth2.service; - - -import com.rollthedice.backend.domain.member.entity.Member; -import com.rollthedice.backend.domain.member.entity.SocialType; -import com.rollthedice.backend.domain.member.repository.MemberRepository; -import com.rollthedice.backend.global.oauth2.CustomOAuth2User; -import com.rollthedice.backend.global.oauth2.OAuthAttributes; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -public class CustomOAuth2UserService implements OAuth2UserService { - private final MemberRepository memberRepository; - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - log.info("CustomOAuth2UserService.loadUser() ์‹คํ–‰ - OAuth2 ๋กœ๊ทธ์ธ ์š”์ฒญ ์ง„์ž…"); - - OAuth2UserService delegate = new DefaultOAuth2UserService(); - OAuth2User oAuth2User = delegate.loadUser(userRequest); - - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - SocialType socialType = getSocialType(registrationId); - String userNameAttributeName = userRequest.getClientRegistration() - .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - Map attributes = oAuth2User.getAttributes(); - - OAuthAttributes extractAttributes = - OAuthAttributes.of(socialType, userNameAttributeName, attributes); - - Member createdMember = getMember(extractAttributes, socialType); - - // DefaultOAuth2User๋ฅผ ๊ตฌํ˜„ํ•œ CustomOAuth2User ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋ฐ˜ํ™˜ - return new CustomOAuth2User( - Collections.singleton(new SimpleGrantedAuthority(createdMember.getRole().getString())), - attributes, - extractAttributes.getNameAttributeKey(), - createdMember.getEmail(), - createdMember.getRole() - ); - } - - private SocialType getSocialType(String registrationId) { - return SocialType.KAKAO; - } - - private Member getMember(OAuthAttributes attributes, SocialType socialType) { - Member findMember = memberRepository.findBySocialTypeAndOauthId(socialType, - attributes.getOauth2UserInfo().getId()).orElse(null); - if(findMember == null) { - return saveMember(attributes, socialType); - } - return findMember; - } - - private Member saveMember(OAuthAttributes attributes, SocialType socialType) { - Member createdUser = attributes.toEntity(socialType, attributes.getOauth2UserInfo()); - return memberRepository.save(createdUser); - } -} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/OAuth2ProviderService.java b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/OAuth2ProviderService.java new file mode 100644 index 00000000..a77aae75 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/oauth2/service/OAuth2ProviderService.java @@ -0,0 +1,45 @@ +package com.rollthedice.backend.global.oauth2.service; + +import com.rollthedice.backend.global.oauth2.dto.LoginRequest; +import com.rollthedice.backend.global.oauth2.userInfo.KakaoOAuth2UserInfo; +import com.rollthedice.backend.global.oauth2.userInfo.OAuth2UserInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + + +import java.util.Map; + +import static com.rollthedice.backend.domain.member.entity.SocialType.KAKAO; + + +@Service +@RequiredArgsConstructor +public class OAuth2ProviderService { + + public OAuth2UserInfo getUserInfo(LoginRequest request) { + return switch (request.getSocialType()) { + case APPLE -> getKakaoUserInfo(request); //apple ๋กœ๊ทธ์ธ ๊ตฌํ˜„ ๋ฏธ์™„์„ฑ + case KAKAO -> getKakaoUserInfo(request); + }; + } + + private OAuth2UserInfo getKakaoUserInfo(LoginRequest request) { + Map attributes = WebClient.create(KAKAO.getProviderUrl()) + .get() + .headers(httpHeaders -> { + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + httpHeaders.setBearerAuth(request.getToken()); + }) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(Map.class) + .log() + .block(); + + return new KakaoOAuth2UserInfo(attributes); + } + +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/resolver/LoginMemberArgumentResolver.java b/backend/core/src/main/java/com/rollthedice/backend/global/resolver/LoginMemberArgumentResolver.java new file mode 100644 index 00000000..8890b2de --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/resolver/LoginMemberArgumentResolver.java @@ -0,0 +1,33 @@ +package com.rollthedice.backend.global.resolver; + +import com.rollthedice.backend.global.annotation.LoginMemberEmail; +import com.rollthedice.backend.global.security.jwt.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +@Component +@Slf4j +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver{ + private final JwtService jwtService; + + @Override + + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.hasParameterAnnotation(LoginMemberEmail.class); + } + + @Override + public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) { + HttpServletRequest request = (HttpServletRequest)nativeWebRequest.getNativeRequest(); + return jwtService.getEmail(request); + } +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/InvalidTokenException.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/InvalidTokenException.java new file mode 100644 index 00000000..7ccce55e --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/InvalidTokenException.java @@ -0,0 +1,14 @@ +package com.rollthedice.backend.global.security.jwt.exception; + +public class InvalidTokenException extends RuntimeException { + public InvalidTokenException() { + } + + public InvalidTokenException(String message) { + super(message); + } + + public InvalidTokenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/NotFoundEmailException.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/NotFoundEmailException.java new file mode 100644 index 00000000..c51a4009 --- /dev/null +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/NotFoundEmailException.java @@ -0,0 +1,15 @@ +package com.rollthedice.backend.global.security.jwt.exception; + +public class NotFoundEmailException extends RuntimeException{ + public NotFoundEmailException() { + } + + public NotFoundEmailException(String message) { + super(message); + } + + public NotFoundEmailException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/exception/NotFoundTokenException.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/NotFoundTokenException.java similarity index 82% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/exception/NotFoundTokenException.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/NotFoundTokenException.java index 658192ee..7ef6f50d 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/exception/NotFoundTokenException.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/exception/NotFoundTokenException.java @@ -1,4 +1,4 @@ -package com.rollthedice.backend.global.jwt.exception; +package com.rollthedice.backend.global.security.jwt.exception; public class NotFoundTokenException extends RuntimeException { public NotFoundTokenException() { diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/filter/JwtAuthenticationProcessingFilter.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/filter/JwtAuthenticationProcessingFilter.java similarity index 50% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/filter/JwtAuthenticationProcessingFilter.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/filter/JwtAuthenticationProcessingFilter.java index e47cd54a..f7d3baf6 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/filter/JwtAuthenticationProcessingFilter.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/filter/JwtAuthenticationProcessingFilter.java @@ -1,11 +1,12 @@ -package com.rollthedice.backend.global.jwt.filter; +package com.rollthedice.backend.global.security.jwt.filter; import com.rollthedice.backend.domain.member.entity.Member; import com.rollthedice.backend.domain.member.repository.MemberRepository; -import com.rollthedice.backend.global.jwt.refresh.domain.RefreshToken; -import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService; -import com.rollthedice.backend.global.jwt.service.JwtService; -import com.rollthedice.backend.global.jwt.util.PasswordUtil; +import com.rollthedice.backend.global.security.jwt.exception.InvalidTokenException; +import com.rollthedice.backend.global.security.jwt.refresh.domain.RefreshToken; +import com.rollthedice.backend.global.security.jwt.refresh.service.RefreshTokenService; +import com.rollthedice.backend.global.security.jwt.service.JwtService; +import com.rollthedice.backend.global.security.jwt.util.PasswordUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -29,10 +30,10 @@ public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter { private static final String NO_CHECK_URL = "/login"; private final JwtService jwtService; - private final RefreshTokenService refreshTokenService; private final MemberRepository memberRepository; + private final RefreshTokenService refreshTokenService; - private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @@ -41,61 +42,59 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); return; } + String refreshToken = jwtService.extractRefreshToken(request) - .filter(jwtService::isTokenValid) .orElse(null); if (refreshToken != null) { checkRefreshTokenAndReIssueAccessToken(response, refreshToken); + response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } - log.info("refresh token is null"); checkAccessTokenAndAuthentication(request, response, filterChain); } public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) { - RefreshToken refresh = refreshTokenService.findByToken(refreshToken); - String reIssuedRefreshToken = reIssueRefreshToken(refresh.getEmail()); - jwtService.sendAccessAndRefreshToken(response, - jwtService.createAccessToken(refresh.getEmail()), reIssuedRefreshToken); - } - - private String reIssueRefreshToken(String email) { - String reIssuedRefreshToken = jwtService.createRefreshToken(); - - refreshTokenService.updateToken(email, reIssuedRefreshToken); - return reIssuedRefreshToken; + if (jwtService.isTokenValid(refreshToken)) { + RefreshToken refresh = refreshTokenService.findByToken(refreshToken); + jwtService.sendAccessAndRefreshToken(response, refresh.getEmail()); + } } - private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - jwtService.extractAccessToken(request) - .filter(jwtService::isTokenValid) - .ifPresent(accessToken -> jwtService.extractEmail(accessToken) - .ifPresent(email -> memberRepository.findByEmail(email) - .ifPresent(this::saveAuthentication))); + public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + log.info("checkAccessTokenAndAuthentication() ํ˜ธ์ถœ"); + try { + jwtService.extractAccessToken(request) + .ifPresent(accessToken -> jwtService.extractEmail(accessToken) + .ifPresentOrElse(email -> memberRepository.findByEmail(email).ifPresent(this::saveAuthentication), + () -> { + throw new InvalidTokenException("Invalid access token"); + } + ) + ); + } catch (Exception e) { + + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } filterChain.doFilter(request, response); } - public void saveAuthentication(Member member) { - String password = member.getPassword(); - if (password == null) { - password = PasswordUtil.generateRandomPassword(); - } + public void saveAuthentication(Member myMember) { + String password = PasswordUtil.generateRandomPassword(); - UserDetails userDetails = User.builder() - .username(member.getEmail()) + UserDetails userDetailsUser = User.builder() + .username(myMember.getEmail()) .password(password) - .roles(member.getRole().name()) + .roles(myMember.getRole().name()) .build(); - Authentication authentication = - new UsernamePasswordAuthenticationToken(userDetails, null, - authoritiesMapper.mapAuthorities(userDetails.getAuthorities())); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null, + authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + SecurityContextHolder.getContext().setAuthentication(authentication); - String name = SecurityContextHolder.getContext().getAuthentication().getName(); - log.info("jwt authentication name : {}", name); } } + diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/domain/RefreshToken.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/domain/RefreshToken.java similarity index 92% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/domain/RefreshToken.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/domain/RefreshToken.java index bd242ba4..5a54aab2 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/domain/RefreshToken.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/domain/RefreshToken.java @@ -1,4 +1,4 @@ -package com.rollthedice.backend.global.jwt.refresh.domain; +package com.rollthedice.backend.global.security.jwt.refresh.domain; import lombok.AccessLevel; import lombok.Getter; diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/repository/RefreshTokenRepository.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/repository/RefreshTokenRepository.java similarity index 61% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/repository/RefreshTokenRepository.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/repository/RefreshTokenRepository.java index 7055e936..2d52ed21 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/repository/RefreshTokenRepository.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/repository/RefreshTokenRepository.java @@ -1,6 +1,6 @@ -package com.rollthedice.backend.global.jwt.refresh.repository; +package com.rollthedice.backend.global.security.jwt.refresh.repository; -import com.rollthedice.backend.global.jwt.refresh.domain.RefreshToken; +import com.rollthedice.backend.global.security.jwt.refresh.domain.RefreshToken; import org.springframework.data.repository.CrudRepository; import java.util.Optional; diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/service/RefreshTokenService.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/service/RefreshTokenService.java similarity index 76% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/service/RefreshTokenService.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/service/RefreshTokenService.java index b9b878fb..d779e955 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/refresh/service/RefreshTokenService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/refresh/service/RefreshTokenService.java @@ -1,8 +1,8 @@ -package com.rollthedice.backend.global.jwt.refresh.service; +package com.rollthedice.backend.global.security.jwt.refresh.service; -import com.rollthedice.backend.global.jwt.exception.NotFoundTokenException; -import com.rollthedice.backend.global.jwt.refresh.domain.RefreshToken; -import com.rollthedice.backend.global.jwt.refresh.repository.RefreshTokenRepository; +import com.rollthedice.backend.global.security.jwt.exception.NotFoundTokenException; +import com.rollthedice.backend.global.security.jwt.refresh.domain.RefreshToken; +import com.rollthedice.backend.global.security.jwt.refresh.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/service/JwtService.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/service/JwtService.java similarity index 65% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/service/JwtService.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/service/JwtService.java index 0332e118..73c89ee0 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/service/JwtService.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/service/JwtService.java @@ -1,9 +1,12 @@ -package com.rollthedice.backend.global.jwt.service; +package com.rollthedice.backend.global.security.jwt.service; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; import com.rollthedice.backend.domain.member.repository.MemberRepository; -import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService; +import com.rollthedice.backend.global.security.jwt.refresh.service.RefreshTokenService; +import com.rollthedice.backend.global.security.jwt.exception.NotFoundEmailException; +import com.rollthedice.backend.global.security.jwt.exception.NotFoundTokenException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; @@ -11,7 +14,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.Optional; @@ -21,31 +23,35 @@ @Getter @Slf4j public class JwtService { + private static final String ACCESS_TOKEN_SUBJECT = "AccessToken"; + private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken"; + private static final String EMAIL_CLAIM = "email"; + private static final String BEARER = "Bearer "; + + private final MemberRepository memberRepository; + private final RefreshTokenService refreshTokenService; @Value("${jwt.secret-key}") private String secretKey; - @Value("${jwt.access.expiration}") private Long accessTokenExpirationPeriod; - @Value("${jwt.refresh.expiration}") private Long refreshTokenExpirationPeriod; - @Value("${jwt.access.header}") private String accessHeader; - @Value("${jwt.refresh.header}") private String refreshHeader; - private static final String ACCESS_TOKEN_SUBJECT = "AccessToken"; - private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken"; - private static final String EMAIL_CLAIM = "email"; - private static final String BEARER = "Bearer "; + public void sendAccessAndRefreshToken(HttpServletResponse response, String email) { + setTokenHeader(response, accessHeader, createAccessToken(email)); - private final MemberRepository memberRepository; - private final RefreshTokenService refreshTokenService; + String refreshToken = createRefreshToken(); + setTokenHeader(response, refreshHeader, refreshToken); + refreshTokenService.updateToken(email, refreshToken); + log.info("Access Token, Refresh Token ํ—ค๋” ์„ค์ • ์™„๋ฃŒ"); + } - public String createAccessToken(String email) { + private String createAccessToken(String email) { Date now = new Date(); return JWT.create() .withSubject(ACCESS_TOKEN_SUBJECT) @@ -54,7 +60,7 @@ public String createAccessToken(String email) { .sign(Algorithm.HMAC512(secretKey)); } - public String createRefreshToken() { + private String createRefreshToken() { Date now = new Date(); return JWT.create() .withSubject(REFRESH_TOKEN_SUBJECT) @@ -62,45 +68,35 @@ public String createRefreshToken() { .sign(Algorithm.HMAC512(secretKey)); } - public void sendAccessToken(HttpServletResponse response, String accessToken) { - response.setStatus(HttpServletResponse.SC_OK); - response.setHeader(accessHeader, accessToken); - } - - public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) { - response.setStatus(HttpServletResponse.SC_OK); - response.setHeader(accessHeader, accessToken); - response.setHeader(refreshHeader, refreshToken); + public Optional extractRefreshToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(refreshHeader)) + .filter(refreshToken -> refreshToken.startsWith(BEARER)) + .map(refreshToken -> refreshToken.replace(BEARER, "")); } public Optional extractAccessToken(HttpServletRequest request) { return Optional.ofNullable(request.getHeader(accessHeader)) - .filter(accessToken -> accessToken.startsWith(BEARER)) - .map(accessToken -> accessToken.replace(BEARER, "")); - } - - public Optional extractRefreshToken(HttpServletRequest request) { - return Optional.ofNullable(request.getHeader(refreshHeader)) .filter(refreshToken -> refreshToken.startsWith(BEARER)) .map(refreshToken -> refreshToken.replace(BEARER, "")); } - public Optional extractEmail(String accessToken) { + public Optional extractEmail(String accessToken) throws JWTVerificationException { try { - return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey)) - .build() - .verify(accessToken) - .getClaim(EMAIL_CLAIM) - .asString()); + return Optional.ofNullable( + JWT.require(Algorithm.HMAC512(secretKey)).build().verify(accessToken).getClaim(EMAIL_CLAIM).asString()); } catch (Exception e) { log.error("์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); return Optional.empty(); } } - @Transactional - public void updateRefreshToken(String email, String refreshToken) { - refreshTokenService.updateToken(email, refreshToken); + public String getEmail(HttpServletRequest request) { + String accessToken = this.extractAccessToken(request).orElseThrow(NotFoundTokenException::new); + return this.extractEmail(accessToken).orElseThrow(NotFoundEmailException::new); + } + + private void setTokenHeader(HttpServletResponse response, String headerName, String token) { + response.setHeader(headerName, BEARER + token); } public boolean isTokenValid(String token) { @@ -112,10 +108,5 @@ public boolean isTokenValid(String token) { return false; } } - - public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) { - response.setHeader(refreshHeader, BEARER + refreshToken); - } } - diff --git a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/util/PasswordUtil.java b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/util/PasswordUtil.java similarity index 94% rename from backend/core/src/main/java/com/rollthedice/backend/global/jwt/util/PasswordUtil.java rename to backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/util/PasswordUtil.java index a547a787..00f98756 100644 --- a/backend/core/src/main/java/com/rollthedice/backend/global/jwt/util/PasswordUtil.java +++ b/backend/core/src/main/java/com/rollthedice/backend/global/security/jwt/util/PasswordUtil.java @@ -1,4 +1,4 @@ -package com.rollthedice.backend.global.jwt.util; +package com.rollthedice.backend.global.security.jwt.util; import java.util.Random; diff --git a/backend/core/src/test/java/com/rollthedice/backend/domain/auth/AuthTest.java b/backend/core/src/test/java/com/rollthedice/backend/domain/auth/AuthTest.java new file mode 100644 index 00000000..1df44ec6 --- /dev/null +++ b/backend/core/src/test/java/com/rollthedice/backend/domain/auth/AuthTest.java @@ -0,0 +1,109 @@ +package com.rollthedice.backend.domain.auth; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.rollthedice.backend.domain.member.controller.MemberController; +import com.rollthedice.backend.domain.member.dto.MemberUpdateDto; +import com.rollthedice.backend.domain.member.service.MemberService; +import com.rollthedice.backend.global.LoginTest; +import com.rollthedice.backend.global.security.jwt.refresh.domain.RefreshToken; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.Date; + +import static com.rollthedice.backend.domain.member.MemberFixture.MEMBER; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("JWT ์ธ์ฆํ…Œ์ŠคํŠธ์˜ ") +@WebMvcTest(MemberController.class) +public class AuthTest extends LoginTest { + + @MockBean + private MemberService memberService; + + @Test + @DisplayName("Access Token์„ ์ด์šฉํ•œ ์ •์ƒ ๋กœ๊ทธ์ธ") + public void accessToken_๋กœ๊ทธ์ธ_์„ฑ๊ณต() throws Exception { + // when + final ResultActions perform = mockMvc.perform(post("/members").contentType(MediaType.APPLICATION_JSON) + .content(toRequestBody(new MemberUpdateDto("yeonjy", "imageUrl2"))) + .header("Authorization", "Bearer " + accessToken)); + + // then + perform.andExpect(status().isOk()); + } + + @Test + @DisplayName("Access Token ๊ธฐ๊ฐ„ ๋งŒ๋ฃŒ๋กœ ์ธํ•œ ๋ฉค๋ฒ„ ์—…๋ฐ์ดํŠธ ์‹คํŒจ") + public void access_token_๊ธฐ๊ฐ„๋งŒ๋ฃŒ() throws Exception { + // given -> ์‹œ๊ฐ„์ด ๋งŒ๋ฃŒ๋œ accessToken ์ƒ์„ฑ + Date now = new Date(); + String accessToken = JWT.create() + .withSubject("AccessToken") + .withExpiresAt(new Date(now.getTime() - 1000)) + .withClaim("email", MEMBER().getEmail()) + .sign(Algorithm.HMAC512(secretKey)); + + // when + final ResultActions perform = mockMvc.perform(post("/members").contentType(MediaType.APPLICATION_JSON) + .content(toRequestBody(new MemberUpdateDto("yeonjy", "imageUrl2"))) + .header("Authorization", "Bearer " + accessToken)); + + // then + perform.andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Refresh Token ์ „์†ก์œผ๋กœ ์ธํ•œ access_token refresh_token ์žฌ๋ฐœ๊ธ‰") + public void refreshToken๊ณผ_accessToken_์žฌ๋ฐœ๊ธ‰() throws Exception { + // given -> refresh Token ์„ธํŒ… ๋ฐ redis ์—์„œ refresh Token์ด ์žˆ๋Š”์ง€ ์กฐํšŒ + Date now = new Date(); + String refreshToken = JWT.create() + .withSubject("RefreshToken") + .withExpiresAt(new Date(now.getTime() + 18000)) + .sign(Algorithm.HMAC512(secretKey)); + + RefreshToken token = new RefreshToken(MEMBER().getEmail()); + + given(refreshTokenService.findByToken(refreshToken)).willReturn(token); + + // when + final ResultActions perform = mockMvc.perform(post("/members").contentType(MediaType.APPLICATION_JSON) + .content(toRequestBody(new MemberUpdateDto("yeonjy", "imageUrl2"))) + .header("Authorization-refresh", "Bearer " + refreshToken)).andDo(print()); + + // then + perform.andExpect(status().isForbidden()) + .andExpect(header().exists("Authorization")) + .andExpect(header().exists("Authorization-refresh")); + } + + @Test + @DisplayName("Refresh Token ๋งŒ๋ฃŒ") + public void refreshToken_๋งŒ๋ฃŒ() throws Exception { + // given -> ๋งŒ๋ฃŒ๋œ refreshToken ์„ค์ • + Date now = new Date(); + String refreshToken = JWT.create() + .withSubject("RefreshToken") + .withExpiresAt(new Date(now.getTime() - 1000)) + .sign(Algorithm.HMAC512(secretKey)); + + // when + final ResultActions perform = mockMvc.perform(post("/members").contentType(MediaType.APPLICATION_JSON) + .content(toRequestBody(new MemberUpdateDto("yeonjy", "imageUrl2"))) + .header("Authorization-refresh", "Bearer " + refreshToken)).andDo(print()); + + // then + perform.andExpect(status().isForbidden()); + } +} + diff --git a/backend/core/src/test/java/com/rollthedice/backend/domain/member/MemberFixture.java b/backend/core/src/test/java/com/rollthedice/backend/domain/member/MemberFixture.java new file mode 100644 index 00000000..ce19038e --- /dev/null +++ b/backend/core/src/test/java/com/rollthedice/backend/domain/member/MemberFixture.java @@ -0,0 +1,48 @@ +package com.rollthedice.backend.domain.member; + +import com.rollthedice.backend.domain.member.dto.MemberServiceDto; +import com.rollthedice.backend.domain.member.entity.Member; +import com.rollthedice.backend.domain.member.entity.Role; +import com.rollthedice.backend.domain.member.entity.SocialType; + +public class MemberFixture { + public final static Member MEMBER() { + return Member.builder() + .email("yeonjy@ourservice.com") + .role(Role.USER) + .imageUrl("imageUrl") + .nickname("yeonjy") + .socialType(SocialType.KAKAO) + .oauthId("-2") + .build(); + + } + + public final static Member SECOND_MEMBER() { + return Member.builder() + .email("realhsb@ourservice.com") + .role(Role.USER) + .imageUrl("imageUrl") + .nickname("realhsb") + .socialType(SocialType.KAKAO) + .oauthId("-1") + .build(); + } + + public final static MemberServiceDto MEMBER_SERVICE_DTO() { + return MemberServiceDto.builder() + .imageUrl("imageUrl") + .nickname("yeonjy") + .email("yeonjy@ourservice.com") + .build(); + } + + public final static MemberServiceDto UPDATE_MEMBER_SERVICE_DTO() { + return MemberServiceDto.builder() + .imageUrl("imageUrl2") + .nickname("yeonjy") + .email("yeonjy@ourservice.com") + .build(); + } + +} diff --git a/backend/core/src/test/java/com/rollthedice/backend/global/LoginTest.java b/backend/core/src/test/java/com/rollthedice/backend/global/LoginTest.java new file mode 100644 index 00000000..dc64f64d --- /dev/null +++ b/backend/core/src/test/java/com/rollthedice/backend/global/LoginTest.java @@ -0,0 +1,65 @@ +package com.rollthedice.backend.global; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rollthedice.backend.domain.member.entity.Member; +import com.rollthedice.backend.domain.member.repository.MemberRepository; +import com.rollthedice.backend.global.security.jwt.filter.JwtAuthenticationProcessingFilter; +import com.rollthedice.backend.global.security.jwt.refresh.service.RefreshTokenService; +import com.rollthedice.backend.global.security.jwt.service.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.util.Date; + +import static com.rollthedice.backend.domain.member.MemberFixture.MEMBER; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@WebMvcTest +public abstract class LoginTest { + protected MockMvc mockMvc; + @Value("${jwt.secret-key}") + protected String secretKey; + protected String accessToken; + @SpyBean + protected JwtService jwtService; + @MockBean + protected RefreshTokenService refreshTokenService; + @MockBean + protected MemberRepository memberRepository; + protected Member loginMember; + @Autowired + private ObjectMapper objectMapper; + + protected String toRequestBody(Object value) throws JsonProcessingException { + return objectMapper.writeValueAsString(value); + } + + @BeforeEach + public void loginSetup(WebApplicationContext ctx) { + mockMvc = MockMvcBuilders + .webAppContextSetup(ctx) + .addFilter(new JwtAuthenticationProcessingFilter(jwtService, memberRepository, refreshTokenService)) + .alwaysDo(print()) + .build(); + + loginMember = MEMBER(); + + Date now = new Date(); + accessToken = JWT.create() + .withSubject("AccessToken") + .withExpiresAt(new Date(now.getTime() + 18000)) + .withClaim("email", loginMember.getEmail()) + .sign(Algorithm.HMAC512(secretKey)); + } +} + diff --git a/iOS/RollTheDice/RollTheDice.xcodeproj/project.pbxproj b/iOS/RollTheDice/RollTheDice.xcodeproj/project.pbxproj index d2c4dcb0..f1ca2bac 100644 --- a/iOS/RollTheDice/RollTheDice.xcodeproj/project.pbxproj +++ b/iOS/RollTheDice/RollTheDice.xcodeproj/project.pbxproj @@ -7,11 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 357666102BBD4BF6002C226A /* StatisticsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3576660F2BBD4BF6002C226A /* StatisticsListView.swift */; }; + 357666102BBD4BF6002C226A /* ReportListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3576660F2BBD4BF6002C226A /* ReportListView.swift */; }; 357666132BBD54AA002C226A /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357666122BBD54AA002C226A /* SplashView.swift */; }; - 357666152BBD5C04002C226A /* FieldStatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357666142BBD5C04002C226A /* FieldStatisticsView.swift */; }; - 357666172BBD5C5B002C226A /* DailyStatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357666162BBD5C5B002C226A /* DailyStatisticsView.swift */; }; - 3576661B2BBD65C3002C226A /* FieldStatisticsReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3576661A2BBD65C3002C226A /* FieldStatisticsReportView.swift */; }; 357FC6EA2BCE866B00AD8915 /* DetailCardNews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357FC6E92BCE866B00AD8915 /* DetailCardNews.swift */; }; 35C71BF22B79F39900F777D1 /* ExyteChat in Frameworks */ = {isa = PBXBuildFile; productRef = 35C71BF12B79F39900F777D1 /* ExyteChat */; }; 6C32379F2B7C376D00B699AB /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C32379E2B7C376D00B699AB /* Bookmark.swift */; }; @@ -24,6 +21,8 @@ 6C3237B22B7C385000B699AB /* NewsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3237B12B7C385000B699AB /* NewsListViewModel.swift */; }; 6C3237B52B7C433D00B699AB /* ChatTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3237B42B7C433D00B699AB /* ChatTypeView.swift */; }; 6C3237B72B7C434600B699AB /* ChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3237B62B7C434600B699AB /* ChatType.swift */; }; + 6C41B8D22BDE696200274FA4 /* NewsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C41B8D12BDE696200274FA4 /* NewsType.swift */; }; + 6C41B8D42BDE6D2500274FA4 /* TypePieChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C41B8D32BDE6D2500274FA4 /* TypePieChartView.swift */; }; 6C454A782B9DA657006FD9D0 /* SignUpQuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C454A772B9DA657006FD9D0 /* SignUpQuestionView.swift */; }; 6C454A7A2B9DA67C006FD9D0 /* SignUpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C454A792B9DA67C006FD9D0 /* SignUpViewModel.swift */; }; 6C454A7C2B9DA71C006FD9D0 /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C454A7B2B9DA71C006FD9D0 /* SignUpView.swift */; }; @@ -31,6 +30,8 @@ 6C454A822B9DAFA3006FD9D0 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C454A812B9DAFA3006FD9D0 /* Path.swift */; }; 6C454A842B9DAFCB006FD9D0 /* PathType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C454A832B9DAFCB006FD9D0 /* PathType.swift */; }; 6C454A882B9DB6C2006FD9D0 /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C454A872B9DB6C2006FD9D0 /* CustomNavigationBar.swift */; }; + 6C4F7BAB2BDE50C600ED01DA /* DailyReportModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4F7BAA2BDE50C600ED01DA /* DailyReportModel.swift */; }; + 6C4F7BAD2BDE510900ED01DA /* DailyReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4F7BAC2BDE510900ED01DA /* DailyReportViewModel.swift */; }; 6C77048C2B722686001B17CB /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C77048B2B722686001B17CB /* MainTabView.swift */; }; 6C77048F2B7229B1001B17CB /* NewsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C77048E2B7229B1001B17CB /* NewsListView.swift */; }; 6C7704992B722A20001B17CB /* MainTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7704982B722A20001B17CB /* MainTabViewModel.swift */; }; @@ -58,6 +59,12 @@ 6CDB29FB2BAA07B10081037B /* GPTChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDB29FA2BAA07B10081037B /* GPTChatViewModel.swift */; }; 6CDB29FD2BAA07FD0081037B /* GPTChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDB29FC2BAA07FD0081037B /* GPTChatView.swift */; }; 6CDB29FF2BAA08280081037B /* GPTChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDB29FE2BAA08280081037B /* GPTChatListViewModel.swift */; }; + 6CE1030C2BD56A4000498AA4 /* TypeReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE1030B2BD56A4000498AA4 /* TypeReportView.swift */; }; + 6CE1030E2BD56A5200498AA4 /* TypeReportModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE1030D2BD56A5200498AA4 /* TypeReportModel.swift */; }; + 6CE103102BD56A5B00498AA4 /* TypeReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE1030F2BD56A5B00498AA4 /* TypeReportViewModel.swift */; }; + 6CE103132BD56B1200498AA4 /* DailyReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE103122BD56B1200498AA4 /* DailyReportView.swift */; }; + 6CE103152BD56CA800498AA4 /* DailyBarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE103142BD56CA800498AA4 /* DailyBarChartView.swift */; }; + 6CE1031A2BD57A2500498AA4 /* DebateSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE103192BD57A2500498AA4 /* DebateSummaryView.swift */; }; 6CE2AC122BD43FB900416A02 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE2AC112BD43FB900416A02 /* SignInView.swift */; }; 6CE2AC1B2BD444BB00416A02 /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE2AC1A2BD444BB00416A02 /* OpenAI */; settings = {ATTRIBUTES = (Required, ); }; }; 6CF130AD2BAB0C4400A437B6 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF130AC2BAB0C4400A437B6 /* AuthenticationViewModel.swift */; }; @@ -75,11 +82,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 3576660F2BBD4BF6002C226A /* StatisticsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsListView.swift; sourceTree = ""; }; + 3576660F2BBD4BF6002C226A /* ReportListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportListView.swift; sourceTree = ""; }; 357666122BBD54AA002C226A /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; - 357666142BBD5C04002C226A /* FieldStatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldStatisticsView.swift; sourceTree = ""; }; - 357666162BBD5C5B002C226A /* DailyStatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyStatisticsView.swift; sourceTree = ""; }; - 3576661A2BBD65C3002C226A /* FieldStatisticsReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldStatisticsReportView.swift; sourceTree = ""; }; 357FC6E92BCE866B00AD8915 /* DetailCardNews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCardNews.swift; sourceTree = ""; }; 6C32379E2B7C376D00B699AB /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; 6C3237A02B7C377600B699AB /* BookmarkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; @@ -91,6 +95,8 @@ 6C3237B12B7C385000B699AB /* NewsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListViewModel.swift; sourceTree = ""; }; 6C3237B42B7C433D00B699AB /* ChatTypeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTypeView.swift; sourceTree = ""; }; 6C3237B62B7C434600B699AB /* ChatType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatType.swift; sourceTree = ""; }; + 6C41B8D12BDE696200274FA4 /* NewsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsType.swift; sourceTree = ""; }; + 6C41B8D32BDE6D2500274FA4 /* TypePieChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypePieChartView.swift; sourceTree = ""; }; 6C454A772B9DA657006FD9D0 /* SignUpQuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpQuestionView.swift; sourceTree = ""; }; 6C454A792B9DA67C006FD9D0 /* SignUpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewModel.swift; sourceTree = ""; }; 6C454A7B2B9DA71C006FD9D0 /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; @@ -98,6 +104,8 @@ 6C454A812B9DAFA3006FD9D0 /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = ""; }; 6C454A832B9DAFCB006FD9D0 /* PathType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathType.swift; sourceTree = ""; }; 6C454A872B9DB6C2006FD9D0 /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = ""; }; + 6C4F7BAA2BDE50C600ED01DA /* DailyReportModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyReportModel.swift; sourceTree = ""; }; + 6C4F7BAC2BDE510900ED01DA /* DailyReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyReportViewModel.swift; sourceTree = ""; }; 6C77048B2B722686001B17CB /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 6C77048E2B7229B1001B17CB /* NewsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListView.swift; sourceTree = ""; }; 6C7704982B722A20001B17CB /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = ""; }; @@ -127,6 +135,12 @@ 6CDB29FA2BAA07B10081037B /* GPTChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPTChatViewModel.swift; sourceTree = ""; }; 6CDB29FC2BAA07FD0081037B /* GPTChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPTChatView.swift; sourceTree = ""; }; 6CDB29FE2BAA08280081037B /* GPTChatListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPTChatListViewModel.swift; sourceTree = ""; }; + 6CE1030B2BD56A4000498AA4 /* TypeReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeReportView.swift; sourceTree = ""; }; + 6CE1030D2BD56A5200498AA4 /* TypeReportModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeReportModel.swift; sourceTree = ""; }; + 6CE1030F2BD56A5B00498AA4 /* TypeReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeReportViewModel.swift; sourceTree = ""; }; + 6CE103122BD56B1200498AA4 /* DailyReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyReportView.swift; sourceTree = ""; }; + 6CE103142BD56CA800498AA4 /* DailyBarChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyBarChartView.swift; sourceTree = ""; }; + 6CE103192BD57A2500498AA4 /* DebateSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebateSummaryView.swift; sourceTree = ""; }; 6CE2AC112BD43FB900416A02 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; 6CF130AC2BAB0C4400A437B6 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; 6CF130AE2BAB0C4F00A437B6 /* AuthenticatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedView.swift; sourceTree = ""; }; @@ -155,16 +169,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 3576660D2BBD4A09002C226A /* Statistics */ = { - isa = PBXGroup; - children = ( - 3576660F2BBD4BF6002C226A /* StatisticsListView.swift */, - 357666182BBD6540002C226A /* FieldStatistics */, - 357666192BBD655A002C226A /* DailyStatistics */, - ); - path = Statistics; - sourceTree = ""; - }; 357666112BBD5494002C226A /* Splah */ = { isa = PBXGroup; children = ( @@ -173,23 +177,6 @@ path = Splah; sourceTree = ""; }; - 357666182BBD6540002C226A /* FieldStatistics */ = { - isa = PBXGroup; - children = ( - 357666142BBD5C04002C226A /* FieldStatisticsView.swift */, - 3576661A2BBD65C3002C226A /* FieldStatisticsReportView.swift */, - ); - path = FieldStatistics; - sourceTree = ""; - }; - 357666192BBD655A002C226A /* DailyStatistics */ = { - isa = PBXGroup; - children = ( - 357666162BBD5C5B002C226A /* DailyStatisticsView.swift */, - ); - path = DailyStatistics; - sourceTree = ""; - }; 6C32379D2B7C374E00B699AB /* BookmarkCard */ = { isa = PBXGroup; children = ( @@ -223,6 +210,14 @@ path = ChatType; sourceTree = ""; }; + 6C41B8D02BDE695A00274FA4 /* Report */ = { + isa = PBXGroup; + children = ( + 6C41B8D12BDE696200274FA4 /* NewsType.swift */, + ); + path = Report; + sourceTree = ""; + }; 6C454A762B9DA62C006FD9D0 /* SignUp */ = { isa = PBXGroup; children = ( @@ -237,6 +232,7 @@ 6C454A7F2B9DAF21006FD9D0 /* Model */ = { isa = PBXGroup; children = ( + 6C41B8D02BDE695A00274FA4 /* Report */, 6C454A802B9DAF9F006FD9D0 /* Path */, ); path = Model; @@ -293,12 +289,12 @@ 6C7704882B722647001B17CB /* View */ = { isa = PBXGroup; children = ( + 6CE103092BD56A2B00498AA4 /* Report */, 357666112BBD5494002C226A /* Splah */, 6CF130AB2BAB0C2D00A437B6 /* Authentication */, 6CE2AC102BD43FA800416A02 /* SignIn */, 6C454A762B9DA62C006FD9D0 /* SignUp */, 6C77048A2B72267E001B17CB /* MainTab */, - 3576660D2BBD4A09002C226A /* Statistics */, 6C77048D2B7229A3001B17CB /* News */, 6C7704902B7229B6001B17CB /* Debate */, 6C7704932B7229C4001B17CB /* Bookmark */, @@ -328,6 +324,7 @@ 6C7704902B7229B6001B17CB /* Debate */ = { isa = PBXGroup; children = ( + 6CE103182BD57A1600498AA4 /* Summary */, 6CDB29F72BAA06FB0081037B /* ChatGPT */, 6C3237B32B7C433000B699AB /* ChatType */, ); @@ -441,6 +438,46 @@ path = ChatGPT; sourceTree = ""; }; + 6CE103092BD56A2B00498AA4 /* Report */ = { + isa = PBXGroup; + children = ( + 3576660F2BBD4BF6002C226A /* ReportListView.swift */, + 6CE103112BD56AF700498AA4 /* Daily */, + 6CE1030A2BD56A3200498AA4 /* Type */, + ); + path = Report; + sourceTree = ""; + }; + 6CE1030A2BD56A3200498AA4 /* Type */ = { + isa = PBXGroup; + children = ( + 6CE1030B2BD56A4000498AA4 /* TypeReportView.swift */, + 6C41B8D32BDE6D2500274FA4 /* TypePieChartView.swift */, + 6CE1030D2BD56A5200498AA4 /* TypeReportModel.swift */, + 6CE1030F2BD56A5B00498AA4 /* TypeReportViewModel.swift */, + ); + path = Type; + sourceTree = ""; + }; + 6CE103112BD56AF700498AA4 /* Daily */ = { + isa = PBXGroup; + children = ( + 6C4F7BAA2BDE50C600ED01DA /* DailyReportModel.swift */, + 6C4F7BAC2BDE510900ED01DA /* DailyReportViewModel.swift */, + 6CE103122BD56B1200498AA4 /* DailyReportView.swift */, + 6CE103142BD56CA800498AA4 /* DailyBarChartView.swift */, + ); + path = Daily; + sourceTree = ""; + }; + 6CE103182BD57A1600498AA4 /* Summary */ = { + isa = PBXGroup; + children = ( + 6CE103192BD57A2500498AA4 /* DebateSummaryView.swift */, + ); + path = Summary; + sourceTree = ""; + }; 6CE2AC102BD43FA800416A02 /* SignIn */ = { isa = PBXGroup; children = ( @@ -606,7 +643,9 @@ files = ( 6C454A882B9DB6C2006FD9D0 /* CustomNavigationBar.swift in Sources */, 6CF130BF2BAB783300A437B6 /* APIConstants.swift in Sources */, + 6CE103102BD56A5B00498AA4 /* TypeReportViewModel.swift in Sources */, 6CF130C52BAB79DE00A437B6 /* RollTheDiceAPI.swift in Sources */, + 6CE103132BD56B1200498AA4 /* DailyReportView.swift in Sources */, 6C454A7A2B9DA67C006FD9D0 /* SignUpViewModel.swift in Sources */, 6C3237AA2B7C381500B699AB /* NewsView.swift in Sources */, 6CF130C92BAB7CC200A437B6 /* BaseTargetType.swift in Sources */, @@ -614,17 +653,19 @@ 6CF130AD2BAB0C4400A437B6 /* AuthenticationViewModel.swift in Sources */, 6CDB29F92BAA07350081037B /* GPTChat.swift in Sources */, 6CDB29FD2BAA07FD0081037B /* GPTChatView.swift in Sources */, - 357666102BBD4BF6002C226A /* StatisticsListView.swift in Sources */, + 6C41B8D22BDE696200274FA4 /* NewsType.swift in Sources */, + 357666102BBD4BF6002C226A /* ReportListView.swift in Sources */, + 6C41B8D42BDE6D2500274FA4 /* TypePieChartView.swift in Sources */, 6C3237A12B7C377600B699AB /* BookmarkViewModel.swift in Sources */, 6C3237AC2B7C382200B699AB /* News.swift in Sources */, - 3576661B2BBD65C3002C226A /* FieldStatisticsReportView.swift in Sources */, - 357666152BBD5C04002C226A /* FieldStatisticsView.swift in Sources */, + 6CE1030E2BD56A5200498AA4 /* TypeReportModel.swift in Sources */, 357FC6EA2BCE866B00AD8915 /* DetailCardNews.swift in Sources */, 6CC4DDC92B5574670080E7E8 /* ContentView.swift in Sources */, 6C3237A52B7C37D100B699AB /* BookmarkView.swift in Sources */, 6C3237AE2B7C382E00B699AB /* NewsViewModel.swift in Sources */, 6C77048F2B7229B1001B17CB /* NewsListView.swift in Sources */, 357666132BBD54AA002C226A /* SplashView.swift in Sources */, + 6C4F7BAD2BDE510900ED01DA /* DailyReportViewModel.swift in Sources */, 6C3237A72B7C37E500B699AB /* BookmarkListViewModel.swift in Sources */, 6C454A822B9DAFA3006FD9D0 /* Path.swift in Sources */, 6C94799E2BD3C00C00D5AEEB /* Image.swift in Sources */, @@ -634,20 +675,23 @@ 6CF130B22BAB74BA00A437B6 /* NewsService.swift in Sources */, 6C3237B22B7C385000B699AB /* NewsListViewModel.swift in Sources */, 6C454A782B9DA657006FD9D0 /* SignUpQuestionView.swift in Sources */, - 357666172BBD5C5B002C226A /* DailyStatisticsView.swift in Sources */, 6CA901962BA2EC0100E20259 /* Font.swift in Sources */, 6CF130C22BAB786600A437B6 /* APIHeaderManager.swift in Sources */, 6CDB29FB2BAA07B10081037B /* GPTChatViewModel.swift in Sources */, 6C32379F2B7C376D00B699AB /* Bookmark.swift in Sources */, 6C454A7E2B9DAA3F006FD9D0 /* SignUpFinishView.swift in Sources */, 6C7704A12B722CEB001B17CB /* ProfileView.swift in Sources */, + 6C4F7BAB2BDE50C600ED01DA /* DailyReportModel.swift in Sources */, 6C3237B72B7C434600B699AB /* ChatType.swift in Sources */, + 6CE103152BD56CA800498AA4 /* DailyBarChartView.swift in Sources */, 6CE2AC122BD43FB900416A02 /* SignInView.swift in Sources */, 6CC4DDC72B5574670080E7E8 /* RollTheDiceApp.swift in Sources */, 6C77048C2B722686001B17CB /* MainTabView.swift in Sources */, 6C7704992B722A20001B17CB /* MainTabViewModel.swift in Sources */, 6C454A7C2B9DA71C006FD9D0 /* SignUpView.swift in Sources */, 6CF130C72BAB7B9800A437B6 /* RollTheDiceAPINews.swift in Sources */, + 6CE1031A2BD57A2500498AA4 /* DebateSummaryView.swift in Sources */, + 6CE1030C2BD56A4000498AA4 /* TypeReportView.swift in Sources */, 6C77049D2B722CE0001B17CB /* BookmarkListView.swift in Sources */, 6C77049B2B722A5A001B17CB /* TabType.swift in Sources */, ); diff --git a/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/Contents.json b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportCyan.colorset/Contents.json b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportCyan.colorset/Contents.json new file mode 100644 index 00000000..37a19a22 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportCyan.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD8", + "green" : "0xD0", + "red" : "0x23" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD8", + "green" : "0xD0", + "red" : "0x23" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportGreen.colorset/Contents.json b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportGreen.colorset/Contents.json new file mode 100644 index 00000000..0aeb53c7 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportGreen.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0xDE", + "red" : "0x6F" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0xDE", + "red" : "0x6F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportPink.colorset/Contents.json b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportPink.colorset/Contents.json new file mode 100644 index 00000000..67f870da --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportPink.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x73", + "green" : "0x47", + "red" : "0xF8" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x73", + "green" : "0x47", + "red" : "0xF8" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportPurple.colorset/Contents.json b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportPurple.colorset/Contents.json new file mode 100644 index 00000000..fb5a073e --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportPurple.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x2B", + "red" : "0xC8" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x2B", + "red" : "0xC8" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportYellow.colorset/Contents.json b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportYellow.colorset/Contents.json new file mode 100644 index 00000000..d6fb2665 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Resources/ColorAssets.xcassets/Report/reportYellow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x17", + "green" : "0xCC", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x17", + "green" : "0xCC", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/RollTheDice/RollTheDice/RollTheDiceApp.swift b/iOS/RollTheDice/RollTheDice/RollTheDiceApp.swift index 20d1db1f..1997d440 100644 --- a/iOS/RollTheDice/RollTheDice/RollTheDiceApp.swift +++ b/iOS/RollTheDice/RollTheDice/RollTheDiceApp.swift @@ -14,6 +14,7 @@ struct RollTheDiceApp: App { @StateObject private var pathModel = PathModel() @StateObject var newsListViewModel = NewsListViewModel() + @StateObject var bookmarkListViewModel = BookmarkListViewModel() var body: some Scene { WindowGroup { @@ -23,6 +24,7 @@ struct RollTheDiceApp: App { MainTabView(newsListViewModel: newsListViewModel) .navigationDestination(for: PathType.self, destination: { pathType in + // ๊ฐ ๋ทฐ๋งˆ๋‹ค .navigationBarBackButtonHidden() ์„ค์ •ํ•˜๊ธฐ! switch pathType { case .chatView(isAiMode: true) : GPTChatView() @@ -31,11 +33,25 @@ struct RollTheDiceApp: App { case .chatView(isAiMode: false): Text("user") .navigationBarBackButtonHidden() + case .detailNewsView: + DetailCardNews() + case .typeReportView: + TypeReportView() + case .dailyReportView: + DailyReportView() + case .bookmarkView: + BookmarkListView(bookmarkListViewModel: bookmarkListViewModel) + case .mypageView: + Text("mypageView") + case .debateSummaryView: + DebateSummaryView() } }) } + .environmentObject(pathModel) + } else { SignUpView() } diff --git a/iOS/RollTheDice/RollTheDice/Source/Model/Path/PathType.swift b/iOS/RollTheDice/RollTheDice/Source/Model/Path/PathType.swift index 7cd3d0f5..2ec6fb10 100644 --- a/iOS/RollTheDice/RollTheDice/Source/Model/Path/PathType.swift +++ b/iOS/RollTheDice/RollTheDice/Source/Model/Path/PathType.swift @@ -9,4 +9,13 @@ import Foundation enum PathType: Hashable { case chatView(isAiMode: Bool) + case detailNewsView // ๋‰ด์Šค ์ž์„ธํžˆ ๋ณด๊ธฐ + + case typeReportView // ๋ถ„์•ผ๋ณ„ ๋‰ด์Šค ํ†ต๊ณ„ + case dailyReportView // ์š”์ผ๋ณ„ ๋‰ด์Šค ๊ด€๋žŒ ๊ฐœ์ˆ˜ ํ†ต๊ณ„ + + case debateSummaryView // ํ† ๋ก  ์š”์•ฝ ํŽ˜์ด์ง€ ๋ทฐ + + case bookmarkView // ๋ถ๋งˆํฌ๋ทฐ + case mypageView // ๋งˆ์ดํŽ˜์ด์ง€๋ทฐ } diff --git a/iOS/RollTheDice/RollTheDice/Source/Model/Report/NewsType.swift b/iOS/RollTheDice/RollTheDice/Source/Model/Report/NewsType.swift new file mode 100644 index 00000000..99deabbe --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/Model/Report/NewsType.swift @@ -0,0 +1,52 @@ +// +// NewsReport.swift +// RollTheDice +// +// Created by Subeen on 4/28/24. +// + +import Foundation +import SwiftUI + +enum NewsType { + case politics // ์ •์น˜ + case economy // ๊ฒฝ์ œ + case society // ์‚ฌํšŒ + case living // ์ƒํ™œ/๋ฌธํ™” + case world // ์„ธ๊ณ„ + case science // IT/๊ณผํ•™ + + var desciption: String { + switch self { + case .politics: + "์ •์น˜" + case .economy: + "๊ฒฝ์ œ" + case .society: + "์‚ฌํšŒ" + case .living: + "์ƒํ™œ/๋ฌธํ™”" + case .world: + "์„ธ๊ณ„" + case .science: + "IT/๊ณผํ•™" + } + } + + var color: SwiftUI.Color { + switch self { + case .politics: + Color.reportCyan + case .economy: + Color.reportGreen + case .society: + Color.reportPurple + case .living: + Color.reportYellow + case .world: + Color.primary01 + case .science: + Color.reportPink + } + } +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Bookmark/BookmarkListView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Bookmark/BookmarkListView.swift index f652615a..ad0be59d 100644 --- a/iOS/RollTheDice/RollTheDice/Source/View/Bookmark/BookmarkListView.swift +++ b/iOS/RollTheDice/RollTheDice/Source/View/Bookmark/BookmarkListView.swift @@ -8,43 +8,87 @@ import SwiftUI struct BookmarkListView: View { - @EnvironmentObject var bookmarkListViewModel : BookmarkListViewModel + @EnvironmentObject var pathModel: PathModel + @StateObject var bookmarkListViewModel : BookmarkListViewModel @State var selectedIndex: Int = 0 + var columns: [GridItem] = [ GridItem(), GridItem()] + var body: some View { ZStack { Color.backgroundDark.ignoresSafeArea(.all) - BookmarkListContentView() - .padding(.leading, 20) + ZStack { + bookmarkListView +// BookmarkListContentView() + .padding(.leading, 20) + VStack(spacing: 0) { + + CustomNavigationBar( + title: "๋ถ๋งˆํฌ", + isDisplayLeadingBtn: true, + leadingItems: + [ + (Image(.chevronLeft), { pathModel.paths.popLast() }), + ] + ) + + Spacer() + + } + + Spacer() + } } + .navigationBarBackButtonHidden() } - private struct BookmarkListContentView: View { - @EnvironmentObject var bookmarkListViewModel: BookmarkListViewModel - var columns: [GridItem] = [ GridItem(), GridItem() ] + var bookmarkListView: some View { - fileprivate var body: some View { - ScrollViewReader { value in - ScrollView(.horizontal, showsIndicators: false) { - LazyHGrid(rows: columns, spacing: 10) { - ForEach(bookmarkListViewModel.bookmarkList, id: \.self) { bookmark in - BookmarkView(bookmark: bookmark) + + ScrollViewReader { value in + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: columns, spacing: 10) { + ForEach(bookmarkListViewModel.bookmarkList, id: \.self) { bookmark in + BookmarkView(bookmark: bookmark) // .onTapGesture { // withAnimation { // selectedIndex = index // value.scrollTo(index) // } // } - } } - .padding(.vertical, 90) } + .padding(.vertical, 90) } } } + +// private struct BookmarkListContentView: View { +// @StateObject var bookmarkListViewModel: BookmarkListViewModel +// var columns: [GridItem] = [ GridItem(), GridItem()] +// +// fileprivate var body: some View { +// ScrollViewReader { value in +// ScrollView(.horizontal, showsIndicators: false) { +// LazyHGrid(rows: columns, spacing: 10) { +// ForEach(bookmarkListViewModel.bookmarkList, id: \.self) { bookmark in +// BookmarkView(bookmark: bookmark) +//// .onTapGesture { +//// withAnimation { +//// selectedIndex = index +//// value.scrollTo(index) +//// } +//// } +// } +// } +// .padding(.vertical, 90) +// } +// } +// } +// } } #Preview { - BookmarkListView() + BookmarkListView(bookmarkListViewModel: BookmarkListViewModel()) .environmentObject(BookmarkListViewModel()) } diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Debate/ChatType/ChatTypeView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Debate/ChatType/ChatTypeView.swift index 9c43fc90..f93b8c35 100644 --- a/iOS/RollTheDice/RollTheDice/Source/View/Debate/ChatType/ChatTypeView.swift +++ b/iOS/RollTheDice/RollTheDice/Source/View/Debate/ChatType/ChatTypeView.swift @@ -9,90 +9,143 @@ import SwiftUI struct ChatTypeView: View { @EnvironmentObject private var pathModel: PathModel - @State var isSelected: Bool = false +// @State var isSelected: Bool = false var body: some View { ZStack { Color.backgroundDark.ignoresSafeArea(.all) - ChatTypeContentView(isSelected: $isSelected) + VStack { + CustomNavigationBar( + isDisplayTrailingBtn: true, + trailingItems: [ + (Image(.bookmarkfill), { pathModel.paths.append(.bookmarkView)}), + (Image(.profileWhite), { pathModel.paths.append(.mypageView)}) + ] + ) + Spacer() + recentReadNewsView + Spacer() + + chatListView + + + } + } } - private struct ChatTypeContentView: View { - - @Binding var isSelected: Bool - @EnvironmentObject private var pathModel: PathModel - - fileprivate var body: some View { + var recentReadNewsView: some View { + VStack(alignment: .leading) { + Text("์ตœ๊ทผ์— ๋ณธ ๋‰ด์Šค") + .foregroundStyle(.gray01) + .font(.pretendardBold32) HStack { Button { - isSelected.toggle() - pathModel.paths.append(.chatView(isAiMode: true)) + // isSelected.toggle() + // pathModel.paths.append(.chatView(isAiMode: true)) } label: { RoundedRectangle(cornerRadius: 15) .foregroundStyle(.primary01) - .frame(width: 350, height: 400) - + .frame(width: 200, height: 200) + .overlay { VStack { HStack { - Text("Chat GPT๋ž‘\nํ† ๋ก ํ•˜๊ธฐ") + Text("๊ธฐ์‚ฌ์ œ๋ชฉ") .multilineTextAlignment(.leading) - .font(.system(size: 40, weight: .bold)) + .font(.pretendardBold24) .foregroundStyle(.basicWhite) - - Spacer() } - - Spacer() - + } + } + } + + Button { + // isSelected.toggle() + // pathModel.paths.append(.chatView(isAiMode: true)) + } label: { + RoundedRectangle(cornerRadius: 15) + .foregroundStyle(.primary01) + .frame(width: 200, height: 200) + + .overlay { + VStack { HStack { - Spacer() - Text("๐Ÿค–") - .font(.system(size: 100)) + Text("๊ธฐ์‚ฌ์ œ๋ชฉ") + .multilineTextAlignment(.leading) + .font(.pretendardBold24) + .foregroundStyle(.basicWhite) } } - .padding(.horizontal, 35) - .padding(.bottom, 35) - .padding(.top, 45) - - } } Button { - pathModel.paths.append(.chatView(isAiMode: false)) + // isSelected.toggle() + // pathModel.paths.append(.chatView(isAiMode: true)) } label: { RoundedRectangle(cornerRadius: 15) - .foregroundStyle(.gray01) - .frame(width: 350, height: 400) - + .foregroundStyle(.primary01) + .frame(width: 200, height: 200) + .overlay { VStack { HStack { - Spacer() - Text("์ธ๊ฐ„์ด๋ž‘\nํ† ๋ก ํ•˜๊ธฐ") - .multilineTextAlignment(.trailing) - .font(.system(size: 40, weight: .bold)) - .foregroundStyle(.primary01) + Text("๊ธฐ์‚ฌ์ œ๋ชฉ") + .multilineTextAlignment(.leading) + .font(.pretendardBold24) + .foregroundStyle(.basicWhite) } - - Spacer() - + } + } + } + } + } + } + + var chatListView: some View { + VStack(alignment: .leading) { + + Text("์ฑ„ํŒ… ๋ชฉ๋ก") + .foregroundStyle(.gray01) + .font(.pretendardBold32) + + ScrollView { + HStack { + Button { + // isSelected.toggle() + // pathModel.paths.append(.chatView(isAiMode: true)) + } label: { + RoundedRectangle(cornerRadius: 15) + .foregroundStyle(.primary01) + .frame(width: 600, height: 100) + + .overlay { HStack { - Text("๐Ÿซถ") - .font(.system(size: 100)) + Text("๊ธฐ์‚ฌ์ œ๋ชฉ") + .multilineTextAlignment(.leading) + .font(.pretendardBold24) + .foregroundStyle(.basicWhite) + Spacer() + + } } - .padding(.horizontal, 35) - .padding(.bottom, 35) - .padding(.top, 45) - } + } + Button { + pathModel.paths.append(.debateSummaryView) + } label: { + + Image(.chevronRight) + .padding(10) + .background(.basicBlack) + } } } } } + } #Preview { diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Debate/Summary/DebateSummaryView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Debate/Summary/DebateSummaryView.swift new file mode 100644 index 00000000..8c17b97c --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Debate/Summary/DebateSummaryView.swift @@ -0,0 +1,114 @@ +// +// DebateSummaryView.swift +// RollTheDice +// +// Created by Subeen on 4/22/24. +// + +import SwiftUI + +struct DebateSummaryView: View { + @EnvironmentObject var pathModel: PathModel + + var body: some View { + ZStack { + Color.backgroundDark.ignoresSafeArea(.all) + + VStack { + CustomNavigationBar(title: "ํ† ๋ก ์š”์•ฝ", isDisplayLeadingBtn: true, leadingItems: [(Image(.chevronLeft), { pathModel.paths.popLast() })]) + + sumView + + } + } + .navigationBarBackButtonHidden() + } + + var sumView: some View { + HStack(spacing: 0) { + Rectangle() + .foregroundStyle( + LinearGradient(colors: [.gray02, .gray02, .gray04], startPoint: .leading, endPoint: .trailing) + + ) + .frame(width: 88) + .clipShape(UnevenRoundedRectangle(topLeadingRadius: 20, topTrailingRadius: 20)) + + ZStack { + + UnevenRoundedRectangle(topLeadingRadius: 20, topTrailingRadius: 20) + .offset(x: 30, y: 20) + .foregroundStyle(.gray05) + .padding(.bottom, 20) + Rectangle() + .foregroundStyle( + LinearGradient(colors: [.gray03, .gray02, .gray02, .gray02, .gray02, .gray02, .gray02], startPoint: .leading, endPoint: .trailing) + + ) + .clipShape(UnevenRoundedRectangle(topLeadingRadius: 20, topTrailingRadius: 20)) + .overlay { + UnevenRoundedRectangle(topLeadingRadius: 20, topTrailingRadius: 20) + .stroke(.gray07, lineWidth: 1.0) + } + + + } + // TODO: ๋งํ’์„  ์ˆ˜์ •ํ•˜๊ธฐ + .overlay { + VStack(spacing: 20) { + Rectangle() + .frame(height: 1) + + Text("โ€œ ์ด๋ฒˆ ํ† ๋ก ์˜ ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์•Œ๋ ค์ค„๊ฒŒ์š”! โ€") + .font(.pretendardBold40) + .padding(.top, 40) + + HStack(alignment: .top, spacing: 0) { + UnevenRoundedRectangle(topLeadingRadius: 60, bottomLeadingRadius: 60, bottomTrailingRadius: 60) + .foregroundStyle(.gray03) + .overlay { + Text("์š”์•ฝ") + + } + ZStack(alignment: .topLeading) { + Rectangle() + .frame(width: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/, height: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/) + .foregroundStyle(.gray03) + Circle() + + .trim(from: 0.5, to: 1) + +// .trim(from: 0, to: 0.5) + .frame(width: 200, height: 200) +// .clipped() + .foregroundStyle(.gray02) + + } + + + Image(.scoopGray01) + .resizable() + .scaledToFit() + .frame(height: 400) + +// .frame(height: 350, alignment: .top) +// .clipped() + } + .padding(.horizontal, 40) + + Spacer() + Rectangle() + .frame(height: 1) + } + .padding(.horizontal, 40) + .padding(.vertical, 40) + } + } + .padding(.top, 60) + .padding(.horizontal, 110) + } +} + +#Preview { + DebateSummaryView() +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/MainTab/MainTabView.swift b/iOS/RollTheDice/RollTheDice/Source/View/MainTab/MainTabView.swift index 6c9d2d26..bd9ca7d9 100644 --- a/iOS/RollTheDice/RollTheDice/Source/View/MainTab/MainTabView.swift +++ b/iOS/RollTheDice/RollTheDice/Source/View/MainTab/MainTabView.swift @@ -18,30 +18,39 @@ struct MainTabView: View { ZStack { Color.backgroundDark .ignoresSafeArea(.all) - - TabView(selection: $mainTabViewModel.selectedTabItem) { + VStack { + CustomNavigationBar( + isDisplayTrailingBtn: true, + trailingItems: [ + (Image(.bookmarkfill), { pathModel.paths.append(.bookmarkView)}), + (Image(.profileWhite), { pathModel.paths.append(.mypageView)}) + ] + ) - StatisticsListView() - .tabItem { - Image(systemName: "list.bullet.rectangle") - } - .tag(0) - - NewsListView(newsListViewModel: newsListViewModel) - .tabItem { - Image(systemName: "square.3.layers.3d.down.left") - } -// .environmentObject(newsListViewModel) - .tag(1) - - ChatTypeView() -// .environmentObject(pathModel) - .tabItem { - Image(systemName: "message") - } -// .environmentObject(pathModel) - .tag(2) - + TabView(selection: $mainTabViewModel.selectedTabItem) { + + ReportListView() + .tabItem { + Image(systemName: "list.bullet.rectangle") + } + .tag(0) + + NewsListView(newsListViewModel: newsListViewModel) + .tabItem { + Image(systemName: "square.3.layers.3d.down.left") + } + // .environmentObject(newsListViewModel) + .tag(1) + + ChatTypeView() + // .environmentObject(pathModel) + .tabItem { + Image(systemName: "message") + } + // .environmentObject(pathModel) + .tag(2) + + } } } } diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyBarChartView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyBarChartView.swift new file mode 100644 index 00000000..f1b36734 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyBarChartView.swift @@ -0,0 +1,129 @@ +// +// DailyBarChartView.swift +// RollTheDice +// +// Created by Subeen on 4/22/24. +// + + +import SwiftUI +import Charts + +struct DailyBarChartView: View { + + @StateObject var dailyViewModel: DailyReportViewModel + + @State var selectedDay: Date? + @State var selectedView: Int? + + var isPreview: Bool = false + + + var selectedValue: (date: Date, views: Int)? { + if let selectedDay { + for preview in dailyViewModel.dailyReportList.reportList { + if preview.date.formatted(date: .long, time: .omitted) == selectedDay.formatted(date: .long, time: .omitted) { + return (selectedDay, preview.views) + } + } + } + return nil + } + + var body: some View { + VStack(alignment: .leading) { + if !isPreview { + HStack { + VStack(alignment: .leading) { + Text("์ด๋ฒˆ์ฃผ ํ‰๊ท ") + .foregroundStyle(.gray04) + .font(.pretendardBold12) + HStack { + Text(dailyViewModel.averageView) + .foregroundStyle(.basicWhite) + .font(.pretendardBold40) + Text("๊ธฐ์‚ฌ") + .foregroundStyle(.gray04) + .font(.pretendardBold12) + } + } + Spacer() + } + + Spacer() + .frame(height: 100) + } + + + + Chart{ + ForEach(dailyViewModel.dailyReportList.reportList) { day in + BarMark( + x: .value("Day", day.date, unit: .weekdayOrdinal), + y: .value("Views", day.views) + + ) + .cornerRadius(8) + .foregroundStyle(.primary01.gradient) + + //TODO: ๋ฐ” ์„ ํƒ / ๋ฏธ์„ ํƒ์— ๋”ฐ๋ฅธ ๋ง‰๋Œ€ ํˆฌ๋ช…๋„ ์กฐ์ ˆ + .opacity(selectedValue?.date == nil || selectedValue?.date.formatted(date: .numeric, time: .omitted) == selectedDay?.formatted(date: .numeric, time: .omitted) ? 1 : 0.5) + } + + if !isPreview { + if let selectedDay = selectedDay { + if selectedValue != nil { + + RuleMark( + x: .value("Day", selectedDay, unit: .day) + ) + .zIndex(-1) + .annotation( + position: .top, + alignment: .centerLastTextBaseline, + overflowResolution: .init( + x: .fit(to: .chart), + y: .disabled + ) + ) { + popoverView + } + .foregroundStyle(.basicWhite) + } + } + } + } + .padding(isPreview ? 50 : 0) + .chartXAxis(isPreview ? .hidden : .visible) + .chartXAxis { + AxisMarks(values: .automatic(desiredCount: 7)) + } + .chartYAxis(.hidden) + .chartXSelection(value: $selectedDay) + } + } + + @ViewBuilder + var popoverView: some View { + VStack(alignment: .center) { + Text("\(selectedValue?.date.formatted(date: .numeric, time: .omitted) ?? "")") + .font(.pretendardRegular14) + Text("\(selectedValue?.views ?? 0)") + .font(.pretendardBold24) + + } + .padding(10) + .background(.gray06.gradient) + .clipShape( + RoundedRectangle(cornerRadius: 8) + ) + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(.gray05, lineWidth: 1.0) + } + } +} + +#Preview { + DailyBarChartView(dailyViewModel: DailyReportViewModel()) +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportModel.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportModel.swift new file mode 100644 index 00000000..9a5051cc --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportModel.swift @@ -0,0 +1,31 @@ +// +// DailyReportModel.swift +// RollTheDice +// +// Created by Subeen on 4/28/24. +// + +import Foundation +import SwiftUI + +struct DailyReportList: Hashable { + var reportList: [DailyReport] +} + +struct DailyReport: Hashable, Identifiable { + let id = UUID() + let dateStr: String // DateFormatter๋กœ ๋ณ€ํ™˜ + let views: Int + + var date: Date { + // TODO: Format Style ์‚ฌ์šฉํ•ด์„œ String -> Date ์ƒ์„ฑํ•˜๊ธฐ +// let strategy = Date.ParseStrategy(format: "\(month: .twoDigits)๋…„ \(month: .twoDigits)์›” \(day: .defaultDigits)", timeZone: TimeZone(abbreviation: "CDT")!) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy๋…„ MM์›” dd์ผ" + + let convertedDate = dateFormatter.date(from: dateStr)! + + return convertedDate + } +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportView.swift new file mode 100644 index 00000000..20b5db78 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportView.swift @@ -0,0 +1,109 @@ +// +// DailyReportView.swift +// RollTheDice +// +// Created by Subeen on 4/22/24. +// + +import SwiftUI + + +struct DailyReportView: View { + + @EnvironmentObject var pathModel: PathModel + + var body: some View { + ZStack { + Color.backgroundDark.ignoresSafeArea(.all) + + VStack { + /// ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋’ค๋กœ๊ฐ€๊ธฐ + CustomNavigationBar(isDisplayLeadingBtn: true, leadingItems: [(Image(.chevronLeft), {pathModel.paths.popLast()})]) + + Spacer() + + HStack { + statisticsView + reportView + } + .frame(height: 500) + .padding(.horizontal, 110) + + Spacer() + } + } + .navigationBarBackButtonHidden() + } + + + /// ๋ฐ์ผ๋ฆฌ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„ + var statisticsView: some View { + RoundedRectangle(cornerRadius: 16) + .stroke(.basicWhite, lineWidth: /*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/) + .background(.gray07) + .overlay { + DailyBarChartView(dailyViewModel: DailyReportViewModel()) + .padding(50) + } + } + + // TODO : ๋ฐฐ์น˜ ๋ฐ”๊พธ๊ธฐ!! + /// ์ƒ๋‹จ ๋ง‰๋Œ€ + ๋ถ„์•ผ๋ณ„ ๋‰ด์Šค ํ†ต๊ณ„ ๋ฐ•์Šค + ํ†ต๊ณ„ ๊ธ€ + ์บ๋ฆญํ„ฐ + /// ํ•˜๋‹จ ๋ง‰๋Œ€ + /// ๊ฐ๊ฐ ๋ฌถ์–ด์„œ ZStack์— ๋ฐฐ์น˜ํ•˜๊ธฐ + var reportView: some View { + RoundedRectangle(cornerRadius: 16) + .foregroundStyle(.gray02) + .overlay { + ZStack { + VStack { + Rectangle() + .frame(height: 1) + + + Rectangle() + .stroke(lineWidth: /*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/) + .frame(height: 52) + .overlay { + Text("์š”์ผ๋ณ„ ๋‰ด์Šค ๊ด€๋žŒ ๊ฐœ์ˆ˜ ํ†ต๊ณ„") + .font(.pretendardBold24) + .foregroundStyle(.gray06) + } + .padding(.vertical, 20) + + + HStack { + Text("์•ˆ๋…•ํ•˜์„ธ์š”. ์ด๊ฒƒ์€ ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค. ์•ˆ๋…•ํ•˜์„ธ์š”. ์ด๊ฒƒ์€ ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค. ") + .multilineTextAlignment(.leading) + + Spacer() + } + .padding(.horizontal, 4) + + Spacer() + + Rectangle() + .frame(height: 1) + } + .padding(.vertical, 34) + .padding(.horizontal, 24) + .foregroundStyle(.gray06) + + HStack { + Spacer() + VStack { + Spacer() + Image(.scoopGray04) + .aspectRatio(0.5, contentMode: .fill) + .frame(height: 250, alignment: .top) + .clipped() + } + } + } + } + } +} + +#Preview { + DailyReportView() +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportViewModel.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportViewModel.swift new file mode 100644 index 00000000..344ca3bb --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Daily/DailyReportViewModel.swift @@ -0,0 +1,39 @@ +// +// DailyReportViewModel.swift +// RollTheDice +// +// Created by Subeen on 4/28/24. +// + +import Foundation + +class DailyReportViewModel: ObservableObject{ + @Published var dailyReportList: DailyReportList + + /// ์ผ์ฃผ์ผ ํ‰๊ท  ์กฐํšŒ์ˆ˜ + var averageView: String { + var aver = 0.0 + + for daily in dailyReportList.reportList { + aver += Double(daily.views) + } + + return String(format: "%.1f", aver / 7) + } + + init( + dailyReportList: DailyReportList = .init( + reportList: + [.init(dateStr: "2024๋…„ 1์›” 1์ผ", views: 32), + .init(dateStr: "2024๋…„ 1์›” 2์ผ", views: 2), + .init(dateStr: "2024๋…„ 1์›” 3์ผ", views: 300), + .init(dateStr: "2024๋…„ 1์›” 4์ผ", views: 999), + .init(dateStr: "2024๋…„ 1์›” 5์ผ", views: 12), + .init(dateStr: "2024๋…„ 1์›” 6์ผ", views: 1), + .init(dateStr: "2024๋…„ 1์›” 7์ผ", views: 99), + ] + ) + ) { + self.dailyReportList = dailyReportList + } +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/ReportListView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/ReportListView.swift new file mode 100644 index 00000000..57cc844b --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/ReportListView.swift @@ -0,0 +1,58 @@ +// +// StatisticsListView.swift +// RollTheDice +// +// Created by ์‹ ์˜ˆ์ง„ on 4/3/24. +// + + +import SwiftUI + +struct ReportListView: View { + + @EnvironmentObject var pathModel: PathModel + @State private var selectedSegment = 0 + + var body: some View { + ZStack { + Color.backgroundDark + .ignoresSafeArea(.all) + + HStack { + Button { + pathModel.paths.append(.typeReportView) + } label: { + RoundedRectangle(cornerRadius: 8) + .stroke(.gray01, lineWidth: /*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/) + .background(.gray07) + .overlay { + TypePieChartView(reportViewModel: TypeReportViewModel(), isPreview: true) + } + .frame(width: 400, height: 400) + + } + + Button { + pathModel.paths.append(.dailyReportView) + } label: { + RoundedRectangle(cornerRadius: 8) + .stroke(.gray01, lineWidth: /*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/) + .background(.gray07) + .overlay { + DailyBarChartView(dailyViewModel: DailyReportViewModel(), isPreview: true) + .frame(height: 200) + } + .frame(width: 400, height: 400) + + } + } + + } + + } + +} + +#Preview { + ReportListView() +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypePieChartView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypePieChartView.swift new file mode 100644 index 00000000..bbd26054 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypePieChartView.swift @@ -0,0 +1,55 @@ +// +// TypePieChartView.swift +// RollTheDice +// +// Created by Subeen on 4/28/24. +// + +import SwiftUI +import Charts + +struct TypePieChartView: View { + + @StateObject var reportViewModel: TypeReportViewModel + + var isPreview: Bool = false + + var mostViewed: NewsType { + return reportViewModel.sortedList.first!.newsType + } + + var body: some View { + Chart(reportViewModel.sortedList) { report in + SectorMark( + angle: .value("Views", report.view), + innerRadius: .ratio(0.7), + angularInset: 2.0 + ) + .cornerRadius(8) + .foregroundStyle(report.newsType.color.gradient) + } + /// pie chart์˜ ๊ฐ€์šด๋ฐ ๋ฌธ๊ตฌ + .chartBackground { chartProxy in + GeometryReader { geometry in + let frame = geometry[chartProxy.plotFrame!] + + if !isPreview { + VStack { + Text("๋งŽ์ด ๋ณธ ๋ถ„์•ผ") + .foregroundStyle(.gray05) + .font(.pretendardRegular14) + Text(mostViewed.desciption) + .foregroundStyle(.gray01) + .font(.pretendardBold24) + } + .position(x: frame.midX, y: frame.midY) + } + } + } + .padding(100) + } +} + +#Preview { + TypePieChartView(reportViewModel: TypeReportViewModel()) +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportModel.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportModel.swift new file mode 100644 index 00000000..1c35be6e --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportModel.swift @@ -0,0 +1,25 @@ +// +// TypeReport.swift +// RollTheDice +// +// Created by Subeen on 4/21/24. +// + +import Foundation + +struct TypeReportList: Hashable { + var reportList: [TypeReport] +} + +struct TypeReport: Hashable, Identifiable { + var id = UUID() + var newsType: NewsType + var view: Int + + // case politics // ์ •์น˜ + // case economy // ๊ฒฝ์ œ + // case society // ์‚ฌํšŒ + // case living // ์ƒํ™œ/๋ฌธํ™” + // case world // ์„ธ๊ณ„ + // case science // IT/๊ณผํ•™ +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportView.swift new file mode 100644 index 00000000..5f33b3bf --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportView.swift @@ -0,0 +1,106 @@ +// +// TypeReportView.swift +// RollTheDice +// +// Created by Subeen on 4/21/24. +// + +import SwiftUI + +struct TypeReportView: View { + + @EnvironmentObject var pathModel: PathModel + + var body: some View { + ZStack { + Color.backgroundDark.ignoresSafeArea(.all) + + VStack { + CustomNavigationBar(isDisplayLeadingBtn: true, leadingItems: [(Image(.chevronLeft), {pathModel.paths.popLast()})]) + + Spacer() + + HStack { + statisticsView + reportView + } + .frame(height: 500) + .padding(.horizontal, 110) + + Spacer() + } + + } + .navigationBarBackButtonHidden() + } + + var statisticsView: some View { + + RoundedRectangle(cornerRadius: 16) + .stroke(.basicWhite, lineWidth: /*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/) + .background(.gray07) + .overlay { + TypePieChartView(reportViewModel: TypeReportViewModel()) + } + } + + // TODO : ๋ฐฐ์น˜ ๋ฐ”๊พธ๊ธฐ!! + /// ์ƒ๋‹จ ๋ง‰๋Œ€ + ๋ถ„์•ผ๋ณ„ ๋‰ด์Šค ํ†ต๊ณ„ ๋ฐ•์Šค + ํ†ต๊ณ„ ๊ธ€ + ์บ๋ฆญํ„ฐ + /// ํ•˜๋‹จ ๋ง‰๋Œ€ + /// ๊ฐ๊ฐ ๋ฌถ์–ด์„œ ZStack์— ๋ฐฐ์น˜ํ•˜๊ธฐ + var reportView: some View { + RoundedRectangle(cornerRadius: 16) + .foregroundStyle(.gray02) + .overlay { + ZStack { + VStack { + Rectangle() + .frame(height: 1) + + + Rectangle() + .stroke(lineWidth: /*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/) + .frame(height: 52) + .overlay { + Text("๋ถ„์•ผ๋ณ„ ๋‰ด์Šค ํ†ต๊ณ„") + .font(.pretendardBold24) + .foregroundStyle(.gray06) + } + .padding(.vertical, 20) + + + HStack { + Text("์•ˆ๋…•ํ•˜์„ธ์š”. ์ด๊ฒƒ์€ ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค. ์•ˆ๋…•ํ•˜์„ธ์š”. ์ด๊ฒƒ์€ ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค. ") + .multilineTextAlignment(.leading) + Spacer() + } + .padding(.horizontal, 4) + + Spacer() + + Rectangle() + .frame(height: 1) + } + .padding(.vertical, 34) + .padding(.horizontal, 24) + .foregroundStyle(.gray06) + + HStack { + Spacer() + VStack { + Spacer() + Image(.scoopGray04) + .aspectRatio(0.5, contentMode: .fill) + .frame(height: 250, alignment: .top) + .clipped() + } +// Spacer() + } + } + } + } +} + +#Preview { + TypeReportView() +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportViewModel.swift b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportViewModel.swift new file mode 100644 index 00000000..d23bce97 --- /dev/null +++ b/iOS/RollTheDice/RollTheDice/Source/View/Report/Type/TypeReportViewModel.swift @@ -0,0 +1,33 @@ +// +// TypeReportViewModel.swift +// RollTheDice +// +// Created by Subeen on 4/21/24. +// + +import Foundation + +class TypeReportViewModel: ObservableObject { + + @Published var reportList: TypeReportList + + init( + reportList: TypeReportList = .init( + reportList: [ + .init(newsType: .economy, view: 10), + .init(newsType: .living, view: 20), + .init(newsType: .politics, view: 30), + .init(newsType: .science, view: 5), + .init(newsType: .society, view: 5), + .init(newsType: .world, view: 30) + ] + ) + ) { + self.reportList = reportList + } + + // ๋น„์œจ์ด ๋‚ฎ์€ ์ˆœ์œผ๋กœ ์ •๋ ฌ (ํŒŒ์ด ์ฐจํŠธ์—์„œ ๋ฐ˜์‹œ๊ณ„๋ฐฉํ–ฅ์œผ๋กœ ๊ทธ๋ž˜ํ”„ ์ฐจ์ง€) + var sortedList: [TypeReport] { + return reportList.reportList.sorted { $0.view > $1.view } + } +} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/DailyStatistics/DailyStatisticsView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Statistics/DailyStatistics/DailyStatisticsView.swift deleted file mode 100644 index e8f1e3c6..00000000 --- a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/DailyStatistics/DailyStatisticsView.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// DailyStatisticsView.swift -// RollTheDice -// -// Created by ์‹ ์˜ˆ์ง„ on 4/3/24. -// - -import Foundation -import SwiftUI - -struct DailyStatisticsView: View { - - var body: some View { - ZStack{ - Color.backgroundDark - .ignoresSafeArea(.all) - - ScrollView(.horizontal){ - - HStack{ - - VStack{ - Image("graph2") - - Spacer() - .frame(height: 50) - - Text("๋‰ด์Šค๊ด€๋žŒ๋นˆ๋„") - .bold() - .font(.system(size: 20)) - } - - Spacer() - .frame(width: 100) - - } - - } - - } - } - -} - -#Preview { - DailyStatisticsView() -} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/FieldStatistics/FieldStatisticsReportView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Statistics/FieldStatistics/FieldStatisticsReportView.swift deleted file mode 100644 index b45cafec..00000000 --- a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/FieldStatistics/FieldStatisticsReportView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// FieldStatisticsReportView.swift -// RollTheDice -// -// Created by ์‹ ์˜ˆ์ง„ on 4/3/24. -// - -import SwiftUI - -struct FieldStatisticsListView: View { - - var body: some View { - ZStack { - Rectangle() - .fill(Color.white) - .frame(width: 1000, height: 500) - .cornerRadius(10) - .shadow(radius: 5) - - VStack { - - Image("graph1") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 500, height: 500) - - HStack{ - Text("ํ…์ŠคํŠธ ์˜ˆ์‹œ") - .font(.title) - .foregroundColor(.black) - - } - - } - .padding() - - Spacer() - - - - - - - } - } -} - -struct FieldStatisticsListView_Previews: PreviewProvider { - static var previews: some View { - FieldStatisticsListView() - } -} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/FieldStatistics/FieldStatisticsView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Statistics/FieldStatistics/FieldStatisticsView.swift deleted file mode 100644 index c5a6c2e6..00000000 --- a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/FieldStatistics/FieldStatisticsView.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// FieldStatisticsView.swift -// RollTheDice -// -// Created by ์‹ ์˜ˆ์ง„ on 4/3/24. -// - - -import SwiftUI - -struct FieldStatisticsView: View { - - var body: some View { - ZStack{ - Color.backgroundDark - .ignoresSafeArea(.all) - - ScrollView(.horizontal){ - - HStack{ - - VStack{ - Image("graph1") - - Spacer() - .frame(height: 50) - - Text("์œ ์ €๊ด€์‹ฌ์‚ฌ๋ฐ˜์˜") - .bold() - .font(.system(size: 20)) - } - - Spacer() - .frame(width: 100) - - } - - - - } - - } - - } - -} - -#Preview { - FieldStatisticsView() -} diff --git a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/StatisticsListView.swift b/iOS/RollTheDice/RollTheDice/Source/View/Statistics/StatisticsListView.swift deleted file mode 100644 index 179b5975..00000000 --- a/iOS/RollTheDice/RollTheDice/Source/View/Statistics/StatisticsListView.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// StatisticsListView.swift -// RollTheDice -// -// Created by ์‹ ์˜ˆ์ง„ on 4/3/24. -// - - -import SwiftUI - -struct StatisticsListView: View { - - @State private var selectedSegment = 0 - - var body: some View { - ZStack { - Color.backgroundDark - .ignoresSafeArea(.all) - - VStack { - HStack(spacing: 0) { - ForEach(0 ..< 2) { index in - Text(index == 0 ? "๋ถ„์•ผ๋ณ„ ๋ ˆํฌํŠธ" : "์ผ๋ณ„ ๋ ˆํฌํŠธ") - .bold() - .font(.system(size: 30)) - .frame(maxWidth: .infinity) - .padding() - .foregroundColor(selectedSegment == index ? .white : .primary01) - .background(selectedSegment == index ? Color.primary01 : .clear) - .cornerRadius(10) - .onTapGesture { - selectedSegment = index - } - .tag(index) - } - } - .padding(.leading, 200) - .padding(.trailing, 200) - .padding(.top,10) - - Spacer() - - if selectedSegment == 0 { - FieldStatisticsListView() - } else if selectedSegment == 1 { - DailyStatisticsView() - } - - - Spacer() - } - .padding() - - } - - } - -} - -#Preview { - StatisticsListView() -}